一、问题的起点prompt 工程的缺点做狼人杀 AI 的头几周我对于提示词部分就是用这样方式进行设计的看起来很正常。但当游戏角色从 1 个变成 8 个每个人有不同的规则约束女巫有解药和毒药、守卫不能连守、狼人白天要伪装身份……prompt 越来越多冲突越来越多改一个地方会意外破坏另一个地方的效果。更根本的问题是我不知道这个prompt是不是最好的。调参全靠感觉直觉告诉你这句话放这儿可能更好但没有验证方法没有量化指标模型表现差了也不清楚是prompt问题、数据问题还是模型本身的问题。二、DSPy 是什么一种语言模型编程的方法论DSPy 的文档第一句话通常是“用声明式的方式编程语言模型”。在我的理解中DSPy 把 prompt 工程从手工写文本升级成了定义问题结构 让系统搜索最优解。传统的prompt工程结果判定全靠我们感觉来判断如何那么DSPy是如何进行的呢这不是换了一个工具是换了一种编程思路通过定义目标函数来选择最优解而不是在手动构造解决方案。接下来我们就从DSPy的结构来分别介绍其核心组件。三、Signature把你的问题翻译成 DSPy 能理解的形式签名 (Signatures)以声明式方式定义任务的输入输出规格例如用 question - answer 定义问答任务。编译器会根据签名自动生成和优化提示词替代传统手写提示。我最初的 Signature 是这样写的class SeerAction(dspy.Signature): game_state dspy.InputField() check_target dspy.OutputField()能用但很差。因为game_state是一个随意命名的字段DSPy不知道这个字段里装的是什么数据、格式是什么样的。模型的推理质量取决于模型能否从自然语言描述里推断出字段含义。后来我改成了这样class SeerNightAction(dspy.Signature): 你是狼人杀游戏中的预言家。重要规则必须严格遵守 1. 【禁止自查】check_target 绝不能等于 seer_id。 2. 【只能查存活】check_target 必须是存活玩家。 3. 推理必须结合存活人物情况 别人发言 查验结果 白天投票。 seer_id dspy.InputField( desc你自己的玩家ID例如 Bot5 ) game_state dspy.InputField( desc游戏状态包含存活玩家列表、发言摘要... ) check_target dspy.OutputField( desc查验目标玩家ID必须是存活玩家且不能等于 seer_id ) reasoning dspy.OutputField( desc推理过程必须结合存活人物情况进行分析, prefix推理 )这里有四个关键的设计原则1. 字段名要有语义game_state不如alive_players death_records recent_speeches清晰。字段名越具体DSPy的prompt自动生成质量越高。2. 字段描述desc是prompt的核心desc的内容会直接影响模型对字段的理解。玩家ID不如你自己被分配的玩家标识例如Bot5具体。把格式示例和约束条件都写进去。3. prefix参数控制输出格式reasoning字段加上了prefix推理这让模型的输出天然带有这个前缀在解析时不容易混淆。同理proposal字段用prefix提议speech字段用prefix发言。4. 把规则写进Signature的docstring而不是只放在desc Signature的顶层docstring是模型的系统指令描述的是角色身份和行为规则这是prompt里权重最高的部分。字段级别的desc只描述单个字段两者的作用不同不能互相替代。四、Module把 Signature 变成可执行的推理单元模块 (Modules)封装了特定LLM调用模式的抽象层可自由组合构建复杂流程。常用内置模块包括dspy.ChainOfThought实现思维链、dspy.ReAct构建智能体Agent、dspy.Refine迭代优化输出和 dspy.BestOfN多候选生成并选最优。有了 Signature你需要把它变成一个可以调用函数。DSPy 提供了两种主要方式方式一直接使用dspy.Predict无推理链class SeerModule(dspy.Module): def __init__(self): super().__init__() self.night_action dspy.Predict(SeerNightAction) def forward(self, seer_id, death_records, game_state, known_info, day_vote_info): return self.night_action( seer_idseer_id, death_recordsdeath_records, game_stategame_state, known_infoknown_info, day_vote_infoday_vote_info, )方式二使用dspy.ChainOfThought强制推理链class SeerModule(dspy.Module): def __init__(self): super().__init__() self.night_action dspy.ChainOfThought(SeerNightAction) def forward(self, ...): return self.night_action(...)两者的区别是dspy.Predict直接输出结果dspy.ChainOfThought强制模型先生成推理过程再输出结果。对于狼人杀推理场景ChainOfThought 是更合适的选择因为1. 推理过程本身就是游戏状态评估的一部分可以用来做审计2. ChainOfThought 的中间输出能让 metric 对推理质量做细粒度评估3. 在 DSPy 编译阶段ChainOfThought 更容易被自动优化器识别和调整五、Metric不只是打分是定义问题本身Metric评估指标是一个函数它定义了什么样的输出是好的。它是优化器Teleprompt的评分标准告诉编译器朝哪个方向优化。我最早写的 metric 只有一行def seer_metric(example, pred, traceNone): return 1.0 if pred.check_target example.check_target else 0.0结果模型的准确率上去了但出现自查这个bug。因为这个 metric只衡量答案对不对不衡量有没有违规。改进后的 metric 把规则遵守作为独立维度def _is_rule_compliant(example, pred): if pred.check_target example.seer_id: return False # 自查违规 alive_players _extract_alive_players(example.game_state) if alive_players and pred.check_target not in alive_players: return False # 查死人违规 return True def seer_combined_metric(example, pred, traceNone): rule_score 1.0 if _is_rule_compliant(example, pred) else 0.0 answer_score 1.0 if pred.check_target example.check_target else 0.0 reasoning_score seer_reasoning_quality(example, pred) # 0.4 规则 0.4 答案 0.2 推理质量 return 0.4 * rule_score 0.4 * answer_score 0.2 * reasoning_scoreMetric 的天花板就是系统能力的上限。如果你只衡量对不对系统就只学会答对题。如果你不衡量推理质量系统就只会输出最短的推理路径哪怕那个推理是空洞的。六、Optimizers根据打分选择最优解优化器 (Optimizers)即“编译器”通过分析训练数据和指标自动优化整个程序。常用策略包括BootstrapFewShot生成Few-shot示例、MIPROv2综合优化和COPRO生成并优化任务指令。我一开始使用的是BootstrapFewShot但是每次都只会选择同一个Few-shot导致大模型每次输出的结果都相差不大这里我就准备换一个优化器最终我选择了MIPROv2作为我们项目的优化器这是我们的一个优化器的参数配置optimizer MIPROv2( metricseer_combined_metric, num_candidates10, # 候选 prompt 数量 max_bootstrapped_demos4, # 自助采样的示例上限 max_labeled_demos8, # 直接作为 few-shot 的示例上限 metric_threshold0.85, # 达到 85% 就提前停止 ) compiled optimizer.compile( module(), trainsettrainset, num_trials12, # 搜索次数这里是 compile 的参数 requires_permission_to_runFalse, )写在最后DSPy 教会我的最重要的事用 DSPy 做狼人杀 AI我最大的感受不是“这个工具真好用”而是重新理解了什么叫“定义问题”。传统编程追问的是“怎么让模型输出正确答案”而在 DSPy 的框架里问题变成了“怎么定义什么是正确答案”——听起来差不多实际上天差地别。前者的答案是“写更好的 prompt”后者的答案则是把对问题的理解翻译成可量化的 metric这要求你对问题本身有足够深的把握才能设计出正确的目标函数。DSPy 并不会帮你理解问题它只能在你真正理解问题之后帮你找到更好的解法——这个顺序不能颠倒。