规划和自我检查
同一个多步任务,比较直接做、先列计划再做、做完之后自我检查三种办法。先列计划多花了一倍的钱,效果差不多;自我检查真的找出了两处出处错误,但花了四倍的钱。
- 约 40 分钟
- 难度:进阶
- 实测:2026-09-14 deepseek-flash
讲智能体的文章里,"规划"(planning)和"反思"(reflection)几乎是必讲的两个技巧:先让模型制定计划再执行,执行完再让它检查自己的结果、发现问题就改。听起来很合理,人做复杂的事情也是这样。
但这些技巧都要多调用几次模型。它们到底能带来多少提升,值不值这些钱?这一课用一个真实的多步任务测一测。
任务
"对比 httpx 的 Client 和 AsyncClient 在三件事上的配置方式是否一样:超时、代理、HTTP/2。列成一张表,每一格都注明文档出处(文件名和行号)。"
这个任务比前面的问题复杂:要查三个不同的主题,每个主题要分别看两种客户端,还要准确记下出处。用的还是第 2 课的智能体和三个文档工具。
办法一:直接做
把任务直接交给智能体,它自己决定怎么查。
办法二:先列计划
先调用一次模型,只让它列计划、不许调用工具。然后把计划附在任务后面,交给智能体执行:
def with_plan():
# 第一步:只列计划,不调用工具
plan, usage = model([{"role": "system", "content": SYSTEM}, {"role": "user", "content":
TASK + "\n\n先不要调用工具。列出你打算怎么查,编号列出每一步要找什么,不超过 6 步。"}], None)
answer, messages, stats = loop([
{"role": "system", "content": SYSTEM},
{"role": "user", "content": TASK + "\n\n按这个计划执行,执行中发现计划不对可以调整:\n" + plan.content},
])
……
"执行中发现计划不对可以调整"这一句很重要。计划是在还没查任何资料的时候定的,死板地照着执行,反而会错过查到之后才发现的线索。
办法三:做完再检查
先直接做,拿到回答后,把回答和智能体在过程中查到的全部原文一起交给模型,让它逐格检查:
def with_reflection():
answer, messages, stats = plain()
evidence = "\n\n".join(m["content"] for m in messages if m["role"] == "tool")
review, usage = model([{"role": "user", "content": f"""下面是一份回答和查到的全部原文。逐格检查回答里的表格:
每一格的说法,原文里有没有依据?注明的出处(文件和行号)对不对?
只列出有问题的格子和原因。全部没问题就只回复"没有问题"。
回答:
{answer}
原文:
{evidence[:20000]}"""}], None)
if "没有问题" in review.content[:20]:
return answer, messages, stats
# 有问题就把检查意见交回给智能体,让它继续查、修改回答
messages += [{"role": "assistant", "content": answer},
{"role": "user", "content": "有人检查了你的回答,意见如下。需要的话继续查文档,然后给出修改后的完整回答。\n\n" + review.content}]
revised, messages, more = loop(messages)
……
检查的依据是"查到的原文",也就是工具返回的内容,而不是模型自己的记忆。上一模块第 6 课讲过,评委凭记忆判断会出错,所以要让它对照材料检查。有问题时,把检查意见交回给智能体,它可以继续查文档,然后修改回答。
完整代码见 code/05-agents/planning_reflection.py。
结果
三种办法各跑一次(回答都很长,这里只贴统计和关键部分):
===== 直接做
4 次模型调用,9 次工具调用,13155 词元,7 秒
===== 先列计划
5 次模型调用,13 次工具调用,26086 词元,12 秒
===== 做完再检查
7 次模型调用,14 次工具调用,50132 词元,17 秒
直接做的回答质量已经不错。HTTP/2 一行,它找到了 http2.md 第 50 行的原文 "HTTP/2 support is available on both Client and AsyncClient",明确结论是"一样"。超时和代理两行,它如实说明文档里的示例都只写了 httpx.Client,没有 AsyncClient 的写法,所以只能推断两者一致,"依据有限"。这个分寸把握得很好:它没有把推断当成事实。
有一点小问题:它的回答第一句是英文 "Based on the documentation, here is the comparison table"。智能体读了大量英文文档之后,回答的语言有时会被带偏。在 system 提示词里写明"用中文回答"可以避免。
先列计划多调用了 1 次模型,工具调用从 9 次变成 13 次,词元翻了一倍,时间多了 5 秒。它的计划是这样的:
1. 找 `Client` 的文档,定位它关于超时(timeout)的配置参数与说明。
2. 找 `AsyncClient` 的文档,定位它关于超时(timeout)的配置参数与说明,比对是否一致。
3. 分别在 `Client` 和 `AsyncClient` 文档中找代理(proxy/proxies)相关配置,比对。
4. 分别在 `Client` 和 `AsyncClient` 文档中找 HTTP/2 相关配置(如 http2 参数),比对。
5. 检查两者的基类/继承关系或 API 参考页,确认是否共用同一套初始化参数……
计划的第 5 步带来了一点新东西:它去看了 api.md,发现两个类的成员列表几乎一样,只有 close 和 aclose 不同,并用这一点佐证"两者共用同一套配置参数"。但它的最终结论和直接做的一样:HTTP/2 有明确依据,超时和代理只能推断。多花了一倍的钱,结论没有本质的变化。
做完再检查找到了真问题。检查意见指出了两处出处错误,智能体继续查了之后,在修改后的回答里写明了更正:
### 本次更正的两处(评审意见成立)
1. 原标注 `advanced/timeouts.md:66-68`("`httpx.Timeout` 细调")有误:`:66-68` 只是 `httpx.Timeout(10.0, connect=60.0)` 与 `httpx.Client(timeout=timeout)` 的示例代码。`httpx.Timeout` 的细调说明实际在 `advanced/timeouts.md:41-68`(标题 `## Fine tuning the configuration` 在 `:41`)。……
2. 原标注 `advanced/transports.md:223` 不能作为 `timeout` 构造参数的出处:该行是 `httpx.HTTPTransport(proxy=proxy, **kwargs)`,只涉及 transport 的 `proxy`/`**kwargs`,与客户端 `timeout` 无关。已删除该引用。
我对照了 timeouts.md:第 41 行确实是 "## Fine tuning the configuration" 这个标题,第 66 到 68 行确实只是示例代码。第一处更正是对的。它把第一版回答里一处不相关的引用也删掉了。这种"出处标得不准"的问题,读的人很难自己发现,恰恰是检查最擅长发现的。
代价是:7 次模型调用,5 万个词元,是直接做的将近 4 倍。检查那一步要把所有查到的原文都放进去,这是它贵的主要原因。
但修改后的回答也有一个值得注意的变化:结论从"HTTP/2 一样,超时和代理只能推断"变成了标题上醒目的"三件事上配置方式完全相同"。虽然正文最后仍然说明超时和代理是推断,但标题的语气比证据强了。检查修正了出处,却让结论的措辞变得更肯定。修改之后的回答,也需要再看一遍。
什么时候值得用
一次实验不能下定论,但结合这个结果,我的经验是:
先列计划,适合步骤很多、容易漏掉某一部分的任务,比如"把这 10 个文件都改一遍"。计划能帮模型记住还有哪些没做。在本课这种三五步就能完成的任务上,它的价值不大。另一个用处是给人看:把计划先展示给用户,确认方向对了再执行,比执行完才发现方向错了省钱得多。
做完再检查,适合结果容易出细节错误、而错误的代价比较高的任务,比如引用出处、数字、代码。检查时一定要给它原始材料,让它对照检查,而不是凭记忆。它很贵,所以通常只在最终结果上用一次,不要每一步都检查。
直接做,在大多数任务上是合理的默认选择。先用直接做,建立评估,看看错误主要出在哪里,再针对性地加规划或者检查。
练习
- 在
planning_reflection.py的 SYSTEM 里加上"用中文回答",重新运行,看看"直接做"的回答第一句还会不会是英文。 - 把检查那一步的提示词改成不给原文、只给回答("检查这份回答有没有错误"),重新运行。检查意见的质量有什么变化?
- 把三种办法各跑 3 次,记录每次的词元数和修改的内容。一次的结果和多次的平均值相差大吗?
自测
1. 让模型"先列计划再执行"时,为什么要告诉它"发现计划不对可以调整"?
计划是在还没有查任何资料时制定的,里面的步骤是猜的。执行过程中查到的信息可能说明原计划有问题,死板地照着执行反而会走错方向。
2. 让模型检查自己的回答时,为什么要把查到的原文一起给它?
只给回答,模型只能凭自己的记忆判断对错,而它的记忆可能就是出错的根源,前一个模块的评委就犯过这种错。给了原文,它能逐条对照,发现出处标错、说法没有依据这类具体的问题。
3. 本课的实验里,自我检查找到了真正的错误。那是不是应该在每个任务上都加上自我检查?
不一定。自我检查在本课花了将近 4 倍的钱和 2 倍多的时间。它适合错误代价高、容易出细节错误的任务,而且通常只在最终结果上做一次。另外,修改后的回答也可能引入新问题,比如本课里结论的措辞变得比证据更肯定。
提问与讨论
这一课没看懂的地方,在这里问。看到别人的问题,也欢迎你来回答。
提问 +3 积分,回答别人 +6 积分。内容经审核后公开。
正在加载讨论…