一、前言

上周,我在我们的项目中引入了dspy并使用它进行一个简单的测试,在测试过程中,我进行了几局游戏,发现预言家每次的输出结果都相差不大,这让我在玩起来比较无趣,因为在每个阶段,我都可以预测到他将要说什么,那么我就要想办法进行优化。

二、原因分析

我检查训练数据后,发现每次它的输出都与我给他的训练数据相差不大甚至有的是一摸一样的,这就让我很好奇为什么这个的提示词优化效果如此之差。

经过查阅后,我才知道我使用的 BootstrapFewShot 只是dspy所提供的最基础的编译器,它的一个大体的工作流程为:

1. 从训练集中随机抽取一些样本

2. 让 LLM 自己解决这些样本

3. 挑出 LLM 答对的样本作为 "Demo 示例"

4. 把这些 Demo 附在 Prompt 里,让 LLM 模仿

那么现在就很清晰了,BootstrapFewShot 只是在训练集里挑答案对的,它的Prompt 本身是固定的,它不会去改进指令的写法,这就说明了为什么每次都是差不多的输出。

除此之外,从它的工作流程中也可以看出它的Demo 选取是随机的,不会去考虑如果把其他几道题放在一起会不会有更高的得分,那么说明它选择的提示词并不一定是最优解。现在我们组的狼人杀结构简单并不能进行特别多轮的游戏,我现在还没测出这个问题,但是未来在更多人数的狼人杀,要持续十几天的时候肯定会影响我们的提示词的效果。在未来随着数据集增加,如果数据集中有更合适的结果组合,那么它也不会选取,这是未来的一个很大的隐患,那么我们就要去替换掉这个基础编译器。

那么我先去了解了一下dspy中提供的常见优化器

优化器 特点 适用场景

BootstrapFewShot

简单、从训练集采样 快速原型

COPRO

自动优化 Prompt 指令 指令不够清晰时

SignatureOpt

优化 Field 描述 Signature 设计不优时

MIPROv2

多目标贝叶斯优化

追求最佳效果

TelepromptOPTUNA

超参搜索 大规模调参

这里我们选择了优化效果最好的MIPROv2来作为我们这个提示词的优化器:

MIPRO 全称是 Multi-Instruction Prompt Optimization,是 DSPy 中目前效果最好的优化器之一。它的核心思想是:

在大规模训练集上,用贝叶斯搜索,自动找到最优的 Prompt + 最优的 Few-shot 示例的组合。

它解决了BootstrapFewShot优化器没有解决的问题,即寻找在该问题上的最优的Few-shot示例,这个的实现就是考MIPRO的贝叶斯优化。与简单的暴力穷举去寻找最优的 Few-shot 不同,它在最初时候随机试几种组合,建立"什么样的组合效果好"的直觉。然后,如果随机测试的方案A效果好,那么就在它附近继续探索类似方案。如果在这个区域已经测出高分,它就会集中精力在这个区域深挖,直至找到最优Few-shot示例

与此同时,MIPRO还会优化 Prompt 指令,它会尝试各种不同的指令写法,然后实际运行测试,看哪个组合的得分最高。

从实现原理上可以很明显看出这个优化器与我现在使用的有了很明显的优化,那么我就要进行修改使用这个优化器。

三、应用

这里我们换一下优化器并设置一下默认参数即可。

from dspy.teleprompt import MIPROv2   #换用MIPROv2优化器



    optimizer = MIPROv2(
        metric=seer_accuracy_metric,
        num_threads=30,              # 搜索 30 次找到最优 Prompt + Demo 组合
        max_bootstrapped_demos=4,    # 每次试验最多用 4 条自助采样的示例
        max_labeled_demos=8,         # 固定注入最多 8 条示例到 Prompt 中
        metric_threshold=0.9,
    )

现在,我们的训练数据就显得太少了,优化器在这些数据中很容易选择到重复的数据。我们让ai来根据我们现在6人狼人杀为我们增添一些数据:

现在我们来重新训练一下。嗯,训练大概花了五分钟,确实要比用BootstrapFewShot时间更久,毕竟是从中选择最优few-shot示例嘛,也情有可原,但是训练一次消耗的api有些多,这个我就打算先对预言家这一个部分进行优化,等学校的api发下来我再对其他几个进行优化(钱包有限)。

接下来我们来测试一下,看一下预言家表现如何。

第一晚没有信息,预言家表现正常。经过多次游戏测试,每局预言家都有思考过程变化,并不是重复的,说明效果可以。但是第二晚以后会出现bug,预言家看到消息摘要后会忘记自己是那个机器人,导致会进行自查操作。这与之前设置的游戏规则相违背,虽说设置了替代操作,但是这是我所不想看到的,我就去检查dspy中设置的prompt。

--- 第 1 次尝试 ---
原始 check_target: Bot4
原始 reasoning: Bot4声称昨晚查验了Bot3是好人,而Bot3已经是我已知的金水。Bot4要么是真预言家,要么是狼人悍跳。Bot2跳女巫但信息模糊,Bot1和Bot5发言偏划水。查验Bot4可以确认他的身份:如果他是狼,则直接锁定;如果他是真预言家,则Bot2的狼面上升。
❌ check_target 无效(不在存活列表或为自身)

--- 第 2 次尝试 ---
原始 check_target: Bot4
原始 reasoning: Bot4声称昨晚查验了Bot3是好人,而Bot3已经是我已知的金水。Bot4要么是真预言家,要么是狼人悍跳。Bot2跳女巫但信息模糊,Bot1和Bot5发言偏划水。查验Bot4可以确认他的身份:如果他是狼,则直接锁定;如果他是真预言家,则Bot2的狼面上升。
❌ check_target 无效(不在存活列表或为自身)
这是终端输出,他为什么会查验自己
--- 第 3 次尝试 ---
原始 check_target: Bot4
原始 reasoning: Bot4声称昨晚查验了Bot3是好人,而Bot3已经是我已知的金水。Bot4要么是真预言家,要么是狼人悍跳。Bot2跳女巫但信息模糊,Bot1和Bot5发言偏划水。查验Bot4可以确认他的身份:如果他是狼,则直接锁定;如果他是真预言家,则Bot2的狼面上升。
❌ check_target 无效(不在存活列表或为自身)

这里没有检查出什么大问题,我在dspy的签名中已经写出了它是预言家并说明是几号。

class SeerNightAction(dspy.Signature):
    """
    你是狼人杀游戏中的预言家。你需要在夜晚查验一名玩家的身份。
    重要规则(必须严格遵守):
    1. 【禁止自查】绝对不能查验自己。你的ID是 seer_id,check_target 绝不能等于 seer_id。
    2. 【只能查存活】check_target 必须是【存活玩家列表】中的人,不能查验死人。
    3. 推理必须结合存活人物情况 + 别人发言 + 你的查验结果。
    4. 优先查验:声称是神职但无证据者 > 发言矛盾者 > 跟风投票者。
    5. 发言摘要中会包含自己之前的发言,可能会包含一个人跳出来说自己是预言家的消息,注意检查是不是自己。
    """
    seer_id = dspy.InputField(
        desc="你自己的玩家ID,例如 'Bot5'"
    )
    game_state = dspy.InputField(
        desc="游戏状态,包含:存活玩家列表、死亡玩家及死因..."
    )
    known_info = dspy.InputField(
        desc="你历次查验结果,例如:'第1晚A是狼人;第2晚B是好人'"
    )
    check_target = dspy.OutputField(
        desc="查验目标玩家ID,【必须是存活玩家】且【不能等于 seer_id】。"
    )
    reasoning = dspy.OutputField(
        desc="推理过程,必须结合存活人物情况、别人发言、查验结果进行分析,不能脱离上下文",
        prefix="推理:"
    )

只能说明是ai在生成时丢弃了这部分,只能丢给ai帮我想一下解决办法。它帮我对输出结果添加了更严格的判断,对于不符合要求的结果直接打回重新操作

    if check_target == actor:
        print(f"\n========== 预言家 {actor} 节点执行 ==========")
        print(f"⚠️ LLM 选择了自己,立即强制替换")
        print(f"原始 alive_players: {alive}")
        print(f"原始 roles: {roles}")
        print(f"输入 game_state: {game_state_desc}")
        print(f"输入 known_info: {known_info}")
        print(f"原始 check_target: {check_target}")
        print(f"原始 reasoning: {reasoning}")

        if valid_targets:
            check_target = random.choice(valid_targets)
            reasoning = f"强制替换:{reasoning} | 实际查验 {check_target}"
        else:
            print(f"⚠️ 没有可查验的目标!")
            return {}
        print(f"\n最终结果: check_target={check_target}, reasoning={reasoning}")
        print(f"==========================================\n")
    else:
        print(f"\n========== 预言家 {actor} 节点执行 ==========")
        print(f"原始 alive_players: {alive}")
        print(f"原始 roles: {roles}")
        print(f"输入 game_state: {game_state_desc}")
        print(f"输入 known_info: {known_info}")

        for attempt in range(3):
            print(f"\n--- 第 {attempt + 1} 次尝试 ---")
            print(f"原始 check_target: {check_target}")
            print(f"原始 reasoning: {reasoning}")

            if check_target not in alive or check_target == actor:
                print(f"❌ check_target 无效(不在存活列表或为自身)")
                if valid_targets:
                    check_target = random.choice(valid_targets)
                    reasoning = f"随机查验 {check_target}"
                continue

            if any(pattern in reasoning for pattern in invalid_patterns):
                print(f"❌ reasoning 包含无效内容: {reasoning}")
                logger.warning(f"Seer reasoning 包含无效内容(第{attempt + 1}次): {reasoning}")
                if valid_targets:
                    check_target = random.choice(valid_targets)
                    reasoning = f"随机查验 {check_target}"
                continue

            print(f"✅ 验证通过!")
            break

        if check_target is None or check_target not in alive or check_target == actor:
            print(f"⚠️ 3次尝试均失败,使用 fallback")
            if valid_targets:
                check_target = random.choice(valid_targets)
                reasoning = f"随机查验 {check_target}"
            else:
                print(f"⚠️ 没有可查验的目标!")
                return {}

修正之后,我再进行测试,发现问题出现少了。我再进行多次测试,发现预言家在后续的发言中表现优异。预测结果较准,好人胜率较先前有明显提高。再测试的50局对决中,分析日志可以看到好人胜场达到39场。

四、结语

本周,我们对dspy应用进行优化,与此同时,为训练数据提供更适合的标准,为以后的其他节点的提示词提供了一个可靠的模板,对于其他部分,我们可以使用该节点为模板进行修改验证。后续我们将对metric得分条件进行优化,以一个优秀的标准来评判得分情况。

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐