首页 > 教程攻略 > ai资讯 >基于DSPy和微调,提升Chat模型解决国际象棋难题的能力

基于DSPy和微调,提升Chat模型解决国际象棋难题的能力

来源:互联网 时间:2026-08-24 14:29:15

先说一个很有意思的发现:GPT完成模型在国际象棋这块,表现相当出彩。以gpt-3.5-turbo-instruct为例,它的Elo评级大约能达到1800,已经是一个相当不错的业余棋手水准了。

但轮到聊天模型上场,情况就有点尴尬了——表现通常要差上一截。好在已有研究表明,结合微调和提示优化这两招,能够显著拉升模型的水准。那么问题来了:聊天模型到底能不能通过一番操作,追平甚至逼近完成模型的表现?这就成了这次实验的核心目标。

本文采用的技术路线很明确:用DSPy来做提示优化,再配合微调gpt-4o-mini,两手抓,看看能不能让大型语言模型在国际象棋难题中真正支棱起来。

名词解释

正式开始之前,先把几个关键术语理清楚,方便后面阅读不卡壳。

  • 微调(Finetuning)

    :在特定数据集上对预训练模型进行二次训练,让它更擅长处理某一类任务。
  • 提示优化(Prompt optimization)

    :通过改进输入给AI的提示词,引导它给出更准确、更靠谱的回答。
  • 少样本提示(Few shot prompting)

    :在提示里塞2到5个示例,让模型照着葫芦画瓢。
  • 思维链(Chain of thought,CoT)

    :引导模型把复杂问题拆成一步步推理,中间过程也露出来,而不是直接蹦答案。
  • DSPy

    :一个用来算法化优化语言模型提示和权重的框架,好用但挺费Token。
  • Completion Models

    :专为明确、单一任务设计,能快速生成内容。适合问答系统、代码生成这类场景,代表有GPT-3.5-turbo-instruct、Codex。
  • Chat Models

    :天生为多轮对话而生,能记住上下文。适合虚拟助理、客服系统,GPT-4、GPT-3.5-turbo都属于这一类。

数据集

这次研究依托的是Lichess数据库中的国际象棋难题集,整整400万个带元数据的难题,规模相当可观。对于每个难题,通过API拿到了完整的棋局记录(PGN格式),每一步都有对应的走法和答案。

PGN: 1. e3 e5 2. Ne2 d5 3. Ng3 Nf6 4. Nh5 Bd6 5. d3 Bg4 6.
Answer: ?

处理完成后的数据集已经在Hugging Face上开放获取,感兴趣可以直接找来用。

完成模型能解决国际象棋难题吗?

先拿完成模型跑个基准。结果很干脆:babbage-002在评估集上达到了61.23%的准确率,而更强大的da vinci-002更是直接打到了74.45%。这个数据就是后面所有优化工作的参照系。

模型准确率
babbage-00261.23%
da vinci-00274.45%

提示优化

如果不给任何示例,直接让聊天模型硬上,那成绩简直没法看——gpt-4o-mini只解决了17.4%的难题,gpt-4o稍好一点,也才28.44%。差距有多大,一目了然。

DSPy程序

DSPy:一个用于编程(而不是提示)基础模型的框架

DSPy的精髓在于,可以通过LLM签名和模块来表达程序逻辑,而这个签名经过优化后,能自动转换成高质量的少样本提示。

只靠这一招,gpt-4o-mini的准确率就提升到了25.99%——比起零样本基线,提升了将近50%。在此基础上,再加入一个“非法走棋自我修正”步骤,配合最多2次重试,准确率进一步蹿升到31.72%。

class ChessSolver(dspy.Signature):
    """Given a series of chess moves in Portable Game Notation (PGN) format, your task is to determine and return the correct next move in Standard Algebraic Notation (SAN) format."""
    pgn = dspy.InputField(desc="The chess position")
    answer = dspy.OutputField(desc="The correct next move in SAN format")

class ChessEngine(dspy.Module):
    def __init__(self):
        super().__init__()
        self.generate_move = dspy.ChainOfThought(ChessSolver)

    def forward(self,pgn):
        gen_pred = self.generate_move(pgn=pgn)
        gen_move = gen_pred.answer
        gen_move = gen_move.split(" ")[-1]
        valid, reason = validate_pgn_move(pgn, gen_move)
        dspy.Suggest(valid, reason)
        if valid:
            print(f"valid:\n{pgn} *{gen_move}")
        if not valid:
            print(f"invalid:\n{pgn} *{gen_move}*\n{reason}")

        return dspy.Prediction(pgn=pgn, answer=gen_move, rationale=gen_pred.rationale)

检查走棋是否有效

def validate_pgn_move(pgn_board, san_move):
    # Create a board from the PGN
    board = chess.Board()
    pgn = io.StringIO(pgn_board)
    game = chess.pgn.read_game(pgn)
    
    # Apply all moves from the PGN to get the current board state
    for move in game.mainline_moves():
        board.push(move)
    
    # Parse the new move
    try:
        print(str(san_move))
        chess_move = board.parse_san(str(san_move))
    except chess.InvalidMoveError:
        return False, "Invalid move notation"
    except chess.IllegalMoveError:
        return False, "Illegal move"
    except chess.AmbiguousMoveError:
        return False, "SAN is ambigious"
    
    # Check if the move is legal
    if chess_move in board.legal_moves:
        return True, "Move is valid"
    else:
        return False, "Move is not legal in the current position"

通过DSPy对少样本提示进行优化后,gpt-4o-mini的性能定格在31.72%。而同样的少样本思维链提示换到gpt-4o上,效果直接翻倍——准确率飙到了63.88%。

编译

# 定义超参数:
N = 20 # The number of instructions and fewshot examples that we will generate and optimize over
batches = 50 # The number of optimization trials to be run (we will test out a new combination of instructions and fewshot examples in each trial)
temperature = 1.0 # The temperature configured for generating new instructions

# 设置评估指标
NUM_THREADS = 64

# 评估
metric = dspy.evaluate.answer_exact_match
kwargs = dict(num_threads=NUM_THREADS, display_progress=True)
evaluate = Evaluate(devset=val, metric=metric, **kwargs)

# 基线
baseline_val_score = evaluate(program_with_assertions, devset=val)
print(f"Baseline val: {baseline_val_score}")

# 编译
eval_kwargs = dict(num_threads=NUM_THREADS, display_progress=True, display_table=0)
teleprompter = MIPROv2(prompt_model=prompt_model, task_model=task_model, metric=metric, num_candidates=N, init_temperature=temperature, verbose=True)
compiled_program = teleprompter.compile(program_with_assertions, trainset=train, valset=val, num_batches=batches, max_bootstrapped_demos=3,max_labeled_demos=5, eval_kwargs=eval_kwargs)
compiled_program.sa ve("compiled_chess_cot_ft_student.dspy")

# 编译得分
fs_val_score, fs_outputs = evaluate(compiled_program, devset=val, return_outputs=True)
print(f"Few shot compiled val: {fs_val_score}")

这轮编译消耗了数百万个Token——代价不小,但结果值得。

编译后的提示

下面是DSPy优化后形成的少样本思维链提示,内含5个精选示例:

{
  "generate_move": {
    "lm": null,
    "traces": [],
    "train": [],
    "demos": [
      {
        "augmented": true,
        "pgn": "1. e4 e5 2. Ne2 Bc5 3. Ng3 d6 4. Bc4 h6 5. O-O Nf6 6. d3 O-O 7. Nh5 Bg4 8. Nxf6+ Qxf6 9. ",
        "rationale": "Pgn: 1. e4 e5 2. Ne2 Bc5 3. Ng3 d6 4. Bc4 h6 5. O-O Nf6 6. d3 O-O 7. Nh5 Bg4 8. Nxf6+ Qxf6 9.\nReasoning: Let's think step by step in order to determine the best move for White. After 8...Qxf6, Black has captured the knight on f6, and now White needs to respond. The most logical move for White is to play 9. Qxg4, capturing the bishop on g4. This move not only gains material but also puts pressure on Black's position.",
        "move": "Qxg4"
      },
      {
        "augmented": true,
        "pgn": "1. e4 d5 2. exd5 Qxd5 3. Nc3 Qd8 4. Nf3 Nf6 5. h3 Bf5 6. Bc4 e6 7. d3 Be7 8. Qe2 O-O 9. Bd2 a6 10. g4 Bg6 11. Ne5 b5 12. Bb3 c5 13. h4 h6 14. Nxg6 fxg6 15. g5 Nd5 16. Nxd5 exd5 17. Qe6+ Kh7 18. Bxd5 Bxg5 19. O-O-O Bxd2+ 20. Rxd2 Rf6 21. Qe4 Ra7 22. h5 Re7 23. hxg6+ Kh8 24. Qg2 Qd6 25. Re2 Rxe2 26. Rxh6+ gxh6 27. g7+ Kh7 28. ",
        "rationale": "determine the best move for White. The position after 28. is critical as White has a strong initiative and is threatening to deliver checkmate or win material. The last move played by Black was 27...Kh7, which puts the Black king in a precarious position. \n\nWhite has several options to consider, but the most effective move is to play 29. g8=Q+. This move promotes the pawn on g7 to a queen, delivering check to the Black king. The newly promoted queen will also create a significant threat, as it can potentially lead to checkmate on the next move if Black does not respond adequately.",
        "move": "g8=Q+"
      },
      {
        "pgn": "1. e4 e5 2. Nf3 Nc6 3. Bc4 Bc5 4. c3 Nf6 5. d4 exd4 6. cxd4 Bb4+ 7. Nc3 Nxe4 8. O-O Bxc3 9. d5 Bf6 10. Re1 Ne7 11. Rxe4 d6 12. Bg5 Bxg5 13. Nxg5 h6 14. Nf3 O-O 15. Qe2 Ng6 16. Re1 Bf5 17. Rd4 a6 18. Bd3 Bxd3 19. Qxd3 Qd7 20. h4 Rae8 21. Rxe8 Rxe8 22. h5 Ne5 23. Nxe5 Rxe5 24. g4 Qe7 25. Kg2 Re1 26. Qf5 g6 27. hxg6 fxg6 28. Qxg6+ Qg7 29. Qxg7+ Kxg7 30. Rd2 Kf6 31. f4 Re4 32. Kf3 Rc4 33. b3 Rc5 34. Ke4 Ra5 35. a4 Rc5 36. Rd3 Rc1 37. Rh3 Kg6 38. f5+ Kg5 39. Kf3 Rc3+ 40. Kg2",
        "answer": "Rxh3"
      },
      {
        "pgn": "1. e4 e5 2. f4 exf4 3. Bc4 d6 4. Nc3 h6 5. d4 g5 6. h4 Bg7 7. hxg5 hxg5 8. Rxh8 Bxh8 9. Qh5 Qf6 10. Nd5 Qxd4 11. Nxc7+ Kd8 12. Nf3 Qxe4+ 13. Be2 Kxc7 14. Qxh8 Ne7 15. Nxg5 Qxg2 16. Bxf4 Bg4 17. O-O-O Qxe2 18. Bxd6+ Kb6 19. Qd4+ Kc6 20.",
        "answer": "Qc3+"
      },
      {
        "pgn": "1. e4 e5 2. d3 Nc6 3. Be2 d5 4. exd5 Qxd5 5. Bf3 Qd8 6. Bxc6+ bxc6 7. Nf3 Bd6 8. O-O h6 9. Qe2 Qf6 10. d4 Bg4 11. dxe5 Bxf3 12. exf6+ Bxe2 13. Re1 Nxf6 14. Rxe2+ Be7 15. b3 O-O-O 16. Rxe7",
        "answer": "Rd1+"
      }
    ],
    "signature_instructions": "Given a sequence of chess moves in Portable Game Notation (PGN) format, critically analyze the current board position to determine the next optimal move in Standard Algebraic Notation (SAN) format. Your answer should include a detailed step-by-step reasoning to justify why this move is the best choice, considering the current threats, opportunities for material gain, and positional advantages. Ensure that your rationale explains how your chosen move aligns with an overall strategic plan to improve your position and counter your opponent's threats.",
    "signature_prefix": "Move:"
  }
}

有意思的是,第一个示例里的推理存在一个小错误,实际上重复了说明里的PGN。但实验证明,如果把这个有瑕疵的示例删掉,反而会显著拉低整体性能。有时候,不完美的数据也比没有强。

DSPy编译结果

整理一下提示优化阶段的核心数据:

模型准确率
gpt-4o-mini [zero shot]17.4%
gpt-4o-mini25.99%
gpt-4o-mini [SELF CORRECT 3 tries max]31.72%
gpt-4o [zero shot]28.44%
gpt-4o55.07%
gpt-4o [SELF CORRECT 3 tries max]63.88%

微调

除了优化提示词,另一种思路是在数据上直接微调模型。GPT-3.5-turbo-16k在类似数据上表现不错,所以这次选用了gpt-4o-mini作为微调对象。

构建良好示例

微调成功的关键在于训练数据的质量。Lichess数据库里虽然有400万个难题,但并非每个都适合拿来训练。手动筛选了一批最具代表性的难题,覆盖了开局、中局和残局等各种局面类型。每个示例都包含了完整的PGN棋局、正确的下一步SAN走法,还附加了详细的推理步骤——不光告诉模型“走哪步”,还要让它明白“为什么走这步”。

gpt-4o-mini

gpt-4o-mini是个相对较小的模型,微调前基线只有17.4%。微调后,准确率跃升到45.21%——虽然还是比不上用了提示优化的gpt-4o,但微调的有效性已经实打实地得到了验证。

gpt-4o

gpt-4o体量更大,微调前基线是28.44%。经过DSPy编译的少样本提示优化后,已经能到63.88%。而微调这一步把它的表现又推了一把,直接来到了70.32%——提升空间依然很可观。

da vinci

为了做个全面对比,还尝试了微调da vinci-002——这个模型底子更厚,基线就是74.45%。微调之后,准确率进一步提升到80.12%。即便是强者,也还有精进的空间。

微调结果

完成模型

模型微调前准确率微调后准确率
gpt-4o-mini17.4%45.21%
gpt-4o28.44%70.32%
da vinci-00274.45%80.12%

聊天模型 + DSPy

模型准确度
gpt-4o-mini [zero shot]17.4%
gpt-4o-mini25.99%
gpt-4o-mini [SELF CORRECT 3 tries max]31.72%
gpt-4o-mini finetune57.71%
gpt-4o-mini finetune [SELF CORRECT 3 tries max]

65.64%

gpt-4o [zero shot]28.44%
gpt-4o55.07%
gpt-4o [SELF CORRECT 3 tries max]

63.88%

gpt-4o finetune58.59%
gpt-4o finetune [SELF CORRECT 3 tries max]

71.37%

注意事项

实验数据已经清晰地表明:DSPy编译的少样本提示加上微调,能够显著提升聊天模型在国际象棋难题上的解决能力

但有几个问题得提一下:

第一,微调过程对数据和计算资源的需求都不小,并不是每个人都能轻松搞定的。第二,不同类别的国际象棋难题可能需要不同的优化策略——没有一套方案能包打天下,具体情况还得具体分析。

总结

这项研究的核心结论很明确:通过DSPy提示优化和微调这两条腿走路,聊天模型在国际象棋难题上的表现可以被大幅拉升。在最佳的实验配置下,gpt-4o在经过DSPy优化和微调后,准确率达到了70.32%,已经相当接近完成模型的水准了。

当然,差距还是存在的,完全追上完成模型还有一段路要走。未来的工作会继续探索如何进一步优化这些模型,让它们能更从容地应对各种复杂的盘面。毕竟,目标从来不只是“和完成模型打成平手”,而是真正把聊天模型的潜力榨干。