DeepAgents(二):DeepAgents 的 harness 做了什么
DeepAgents 的 harness 做了什么
DeepAgents 的 harness 干了两件事:装配和分发。装配是把工具、上下文管理、行为规范一层层挂到 Agent 循环上,分发是按模型名决定给哪一套行为规范。你调一次 create_deep_agent,这两件事同时发生。
不传 system_prompt 时,模型收到 8 个工具定义共 2729 token,同时收到一条长度 0 字符的 system 消息。这不是漏传,是 0.7.0 主动删的,弃用信息原文写着「Deep Agents no longer provides an authored base prompt」。
这篇的中心主题就一句:harness 把「怎么装配」全做完了,「Agent 怎么说话」留给你自己写。 那 0 个字符就是这条分工留下的口子。
装配不是写死在函数里的,靠的是一层层中间件,第四节量给你看。
本机跑了 12 次三组对照,结论先给:动作不受影响,措辞从零开始长。同一句请求,空提示词下回你 59 字的道歉加反问,补 80 字提示词后回你 24 字的结果,工具调用序列一次没变。
读完能得到 6 个答案:空是怎么来的、分界线在哪、装配那头给了多少、靠什么挂上去的、空着跑会怎样、要补就补多少。全程只需一台装好 deepagents 的机器,读完约 5 分钟。
一、先把结论摆出来
| 问题 | 实测结论 |
|---|---|
| 提示词真的空吗 | 是。不传就是 0 字符,14 个内置 profile 一个都没补 BASE 位 |
| 是官方漏了吗 | 不是漏,是改成按模型键分发。模型名不在那 14 个里,就是 0 字符 |
| 那装配那头给了多少 | 8 个工具定义 2729 token,是旧提示词 569 token 的 4.8 倍 |
| 这些装配靠什么挂上去 | 中间件。默认挂 11 个实例,工具与上下文管理都由它们提供 |
| 空着跑会崩吗 | 不会。12 次运行全部走完 4 轮循环,动作三组一致 |
| 补多少字够 | 80 字。与官方那 2258 字,在「别客套」这一项上效果相同 |
后面 5 节逐条回答上面这 6 个问题,最后收口。
二、那 0 个字符是怎么来的
提示词按三段拼:USER、BASE、SUFFIX,段与段之间空一行。
USER 是你传的 system_prompt。BASE 取自当前生效的 harness profile 的 base_system_prompt,SUFFIX 取自同一个 profile 的 system_prompt_suffix。
官方文档把结论写得很直白。不传 system_prompt 且 profile 这两项也没写时,「the model receives an empty authored system prompt」。
我注册了一个自己的 profile 验证这段拼接。三种写法各跑一遍,模型收到的是 USER、USER + BASE、USER + SUFFIX,顺序与文档一致,两次运行结果相同。
import deepagents as da
prof = da.HarnessProfile(
base_system_prompt="基准位",
system_prompt_suffix="补充位")
da.register_harness_profile(
"openai:my-model", prof)
顺带踩到一个点:同一个键重复注册是叠加合并,不是覆盖。我注册第二次时,第一次写的内容还留在里面,两段都被拼进了提示词。
三段里 USER 是你的,BASE 与 SUFFIX 归框架。所以问题收窄成一句话:框架往那两位里写了什么。
三、官方没放弃写提示词,它改成了按模型分发
接着上一节那两个位置查。包里注册了 14 个 harness profile,key 形如 anthropic:claude-sonnet-4-6。逐个读出来的结果是:14 个都写了 system_prompt_suffix,写了 base_system_prompt 的是 0 个。
| 模型 | suffix 长度 | 覆盖几个 key |
|---|---|---|
| nemotron-3-ultra | 2962 字符 | 8 |
| claude-opus-4-7 | 2148 字符 | 1 |
| claude-haiku-4-5 与 sonnet-4-6 | 1455 字符 | 2 |
| gpt-5.x-codex | 1166 字符 | 3 |
这些 suffix 写得相当具体,内容是行为规范,不是模型介绍。nemotron 那段拆成 5 个标签,其中 <loop_control> 交代的是:工具调用失败时先读错误、改调用再重试,「never re-issue the same failing call unchanged」。codex 那段更直接:「Do not communicate an upfront plan or status preamble before acting. Just act.」
所以真正的分界线在模型键。模型名不在那 14 个里,profile 匹配不上,BASE 与 SUFFIX 同时是空,落到你头上的就是 0 字符。本机跑的正是这个情况,那条 system 消息的内容就是空字符串。
官方以前不是这么写的。包内留着一个 _LEGACY_BASE_AGENT_PROMPT 常量,2258 字符、约 569 token,当年是一篇通用的基础提示词。
它有 5 个小节:Core Behavior、Professional Objectivity、Doing Tasks、Clarifying Requests、Progress Updates。内容全是行为规范,一句工具说明都没有。摘几句原话:
| 小节 | 原话 |
|---|---|
| Core Behavior | NEVER add unnecessary preamble |
| Core Behavior | If the request is underspecified, ask only the minimum followup needed |
| Doing Tasks | Verify — check your work against what was asked, not against your own output |
| Doing Tasks | If something fails repeatedly, stop and analyze why |
去读 BASE_AGENT_PROMPT 会触发弃用提示,原文写着它在 0.7.0 弃用、0.9.0 移除。所以这个常量只剩兼容访问的用处,不参与装配。
从「一篇通用」改成「按模型键分发」,就是那 0 个字符的直接来路。
四、看另一头:装配给得有多足
提示词那一头是 0,工具那一头是完全另一种待遇。
最小装配给 8 个工具:ls、read_file、write_file、edit_file、delete、glob、grep、task。这 8 个工具的定义加起来 2729 token,拆开看,工具描述 1349 token、参数 schema 1380 token,两半几乎平分。
| 工具 | 合计 | 描述 | 参数 schema |
|---|---|---|---|
| grep | 597 | 144 | 461 |
| task | 499 | 359 | 146 |
| read_file | 456 | 309 | 154 |
| glob | 414 | 207 | 216 |
| edit_file | 293 | 94 | 209 |
| write_file | 191 | 85 | 116 |
| delete | 161 | 95 | 75 |
| ls | 118 | 56 | 70 |
这 8 个工具不是手写进 create_deep_agent 的。harness 把每件事拆成一个中间件,装配的时候一层层挂上去。本机量了一遍默认装配:主 Agent 挂 5 个,子 Agent 挂 5 个,再加一个把 task 桥过去的 SubAgentMiddleware,一共 11 个实例。
工具的来路只有 2 类:
| 工具 | 谁给的 |
|---|---|
ls、read_file、write_file、edit_file、delete、glob、grep |
FilesystemMiddleware |
task |
SubAgentMiddleware |
包里这类中间件一共 27 个,默认只挂 5 类。要加能力也是同一条路:传 skills= 会挂上 SkillsMiddleware,传 memory= 会挂上 MemoryMiddleware。干活的东西都挂在中间件上,提示词那一头就能是空的。
把三样东西放一起量:工具定义 2729 token,官方删掉的那篇提示词 569 token,当前默认提示词 0 token。花在「工具怎么用」上的字,是它当年花在「行为规范」上的 4.8 倍。
那 8 段描述也不只是参数说明,里面写着调度次序。摘几句原话:
| 工具 | 描述里的原话 |
|---|---|
| ls | You should almost ALWAYS use this tool before using the read_file or edit_file tools |
| read_file | Always read a file before editing it |
| edit_file | You must read the file before editing; this tool errors otherwise |
| write_file | Prefer to edit existing files over creating new ones when possible |
拿旧提示词那 5 条规范逐条去工具描述里找,只接走 1 条。
| 旧提示词里的规范 | 工具描述里找得到吗 |
|---|---|
| 先读文件再动手 | 找得到 |
| 不要必要之外的前言 | 找不到 |
| 做完要核对,不看自己的输出 | 找不到 |
| 能力受限时别开头就长篇解释 | 找不到 |
| 长任务给进度更新 | 找不到 |
这条对照把主题的另一半补上了:工具描述能接走「怎么调工具」,接不走「怎么说话、怎么收尾」。 框架在装配那一头把工具调用的次序写满了,说话方式一个字没留。
五、空着跑会怎样:三组 12 次对照
上一节说清了工具有描述、说话没规范,那就把缺的那部分补回来测。
三组配置是:不传提示词、补 4 条短提示词(80 字符)、贴回官方那篇 2258 字符的旧提示词。2 个请求各打一个点。第一个给明确路径但文件不存在,看失败之后怎么说话;第二个不给路径,只问目录里有什么,看它先调哪个工具。
第一个请求,三组都是 4 轮、都只调 read_file。措辞分成三层:
| 组 | 提示词 | 回答 |
|---|---|---|
| 不传 | 0 字符 | 抱歉,我没有找到这个文件。请问您是否想检查其他文件,或者需要我为您创建这个文件呢? |
| 补 4 条 | 80 字符 | 文件未找到。 |
| 官方旧提示词 | 2258 字符 | 文件未找到。请确认文件路径是否正确。 |
第二个请求,三组都是 4 轮、都先调 ls,回答分别是 20 字、14 字、16 字,差距远小于第一个请求。
12 次运行里,工具调用序列三组完全一致,没有一次重复调用同一个失败动作。动作那一栏一次都没分岔,分岔全在措辞那一栏。
有一条得说清楚:温度 0 让重复运行相当稳,但也出现过同一个请求跑出 22 字与 20 字这样的小差异。单次结果不能当结论,这是我留着 12 次的原因。
六、要补就补多少、补在哪
动作层面不用补,措辞层面补 80 字就够。
补 4 条那组与官方那篇 2258 字,在「别客套、直接给结果」这一项上效果相同,都从 59 字收到 24 字与 36 字。补提示词不必补全篇。
补的位置有两种。传 system_prompt 最直接,它落在 USER 位、排在整段最前。要按模型区分,就注册一个自己的 harness profile,写进 base_system_prompt 或 system_prompt_suffix。
动手之前先自查一下。上面那个假模型的做法可以搬走:继承 BaseChatModel、在 _generate 里量一次 system 消息的长度,跑一遍就知道自己拿到的是几位数。
代价也说一下。补上的规范靠模型自己遵守,没有强制性,它不像工具描述那样会在出错时硬拦一次。
第一个请求里,三组都只调一次 read_file 就把结论交回来了,没有一组回头核对路径对不对。旧提示词里那条「做完要核对」,工具描述没接走,我补的 4 条也没写进去,它就真的没发生。
七、结论
harness 替你决定的是装配,提示词它留了口子。
装配那一头做得相当到位:8 个工具、2729 token 的定义、默认 backend、11 个中间件实例,全都装好了,你一句话不写就能跑通完整循环。
提示词那一头按模型键分发。你用它认得的那 14 个模型,白拿一段上千字符的规范;不在名单里,就是 0 字符。
那 0 字符不会让你的 Agent 崩。它会让你的 Agent 用模型自己的习气说话,而小模型的默认习气,是道歉加反问。
所以要写,就写那 80 字。