跳到主要内容

tongyi-deepresearch — 本课题摘录

读了哪几篇: 01-react-loop(主循环)、03-context-and-robustness(上下文管理与容错)。 其余几篇本轮没读。

这一家是本课题最朴素的一份完整实现:一个循环、四种标签、三道闸。适合当我们最小原型的骨架参照。

它对本课题回答了什么

决定二:四种标签,而且分工写清楚了谁写哪个

标签谁写含义
思考模型内心推理
要调工具模型内容是一段结构化参数
工具结果外层代码工具执行结果,回填给模型
答案模型最终答案,出现即收尾

这套约定写死在系统提示里,开头一句就是"当你准备好给出最终回答时,必须把整个答案包进答案标签"。 (依据: shelf=agent/tongyi-deepresearch#01-react-loop @f72f75d8c3eb 事实=模型与外界只通过 /<tool_call>/<tool_response>/ 四种文本标签沟通,其中 tool_response 由外层代码写,约定写死在 SYSTEM_PROMPT 里)

"谁写哪个标签"这一列很重要:四个标签里有一个是外层写的。 模型不该写那一个,但它会写。 所以才有下面这条。

一个必须有的防护:调完模型后,先把"工具结果标签"之后的内容切掉——防模型自己幻觉出观察。

这跟 deepanalyze 的"停在收尾标签"是同一个问题的两种解法:

  • deepanalyze:用停止词让它根本没机会写;
  • tongyi:让它写,然后切掉。

前者更省(不生成就不花钱),后者更简单(不依赖接口支持停止词)。 两条都要知道:自定义格式方案必须处理"模型自己把结果编出来"。

决定四:一个答案标签 + 三道硬闸

退出原因触发
正常交卷内容里出现答案标签
超时单题运行超过 150 分钟
轮数耗尽可用的模型调用次数归零(默认 100)
上下文超长token 数超过 11 万,强制收尾

这是本课题"什么时候停"最完整的一份清单,而且四条互不重叠: 一条是"做完了",三条是"不能再做了"——分别对应时间、次数、空间。

对我们的最小原型,这四条就够了。

决定一最妙的一条:快撑爆时不截断历史,而是逼它现在就交卷

每轮结束都数一次用量,超过阈值就触发一次"最后通牒":把最后一条消息改写成一句强指令,再逼模型立刻用现有信息给出答案。

它的巧妙处:不是粗暴截断历史,而是改写最后一条消息。这样答案仍基于完整证据,只是不再允许继续搜。 (依据:Agent 库 · Tongyi DeepResearch · 上下文管理与容错 —— 每轮数一次 token,超 110K 就把 messages 最后一条内容改写成「已达上下文上限,停止调工具,现在给出答案」的强指令再调一次模型,而非截断历史;收尾后按有没有 answer 标签区分 termination 是「因 token 上限而生成答案」还是「格式错」)

这条是"压缩"之外的第三条路,前面没见过:

  • 压缩:把历史变小,继续跑;
  • 截断:丢掉旧的,继续跑;
  • 交卷:不再跑了,用现有的答。

对我们的最小原型,第三条最省事而且不丢信息。 mirothinker 的"超窗前主动退一步收尾"是同一条。

阈值的选法有讲究:定在 11 万而不是窗口上限 12.8 万,留了约 1.8 万余量给"最后一次回答"本身要占的输入加输出。

这条要记:任何"快满了就收尾"的阈值,都必须给收尾动作本身留出预算。 不留的话,你会在触发收尾的那一刻超窗。

收尾后还区分了两种结果:"因为到顶而生成的答案"和"格式错"。

又一次印证 hermes 那条:停止原因要能说出名字。

决定三:结果以"用户"角色回填

无论哪个工具,结果都套上工具结果标签再作为一条用户消息追加。

拆解文档明确标注:这是这套实现的取舍。

跟 agenticseek 一样的选择,理由也一样——不依赖接口支持专门的工具结果角色。 本课题第三个决定的这个分岔,现在有三家实例了。

循环骨架(可以直接当我们的起点)

消息 = [系统提示, 用户问题]
剩余次数 = 100
while 剩余次数 > 0:
剩余次数 -= 1
内容 = 调模型(消息)
内容 = 切掉「工具结果标签」之后的部分 ← 防幻觉
消息.追加(助手, 内容)
如果 内容里有「要调工具」:
调用 = 抠出标签之间的部分
结果 = 派发执行(调用)
消息.追加(用户, 包成工具结果标签的结果)
如果 内容里有「答案」:
记下「正常交卷」,跳出

这十几行就是本课题的最小骨架。 前面读的三十多家,骨架都可以还原成这十几行; 差别全在这十几行外面挂了什么。

它没回答什么

  • 并发——它一轮只调一个工具。
  • 人机协同——没有审批、没有插话。
  • 状态持久化——跑完即结束,不可续。

坑与代价

  • 每轮都重新加载分词器来数 token。 拆解文档指出这是可优化点——功能正确但重复加载。

    这个细节值得留意:"数 token"这件事在朴素实现里很容易变成每轮的固定开销。 goose 的"用厂商回报的真实用量"能绕开它。

  • 一个常量被定义了两次,后者覆盖前者。 小仓库里也会出现的配置混乱。
  • 150 分钟、100 轮、11 万 token 都是硬编码的经验值。
  • 结果以用户角色回填的代价:模型看到的历史里,"用户"说了很多它自己没说过的话。

    判断(无锚): 对短任务无所谓;长任务里这会让模型对"用户到底要什么"的认知变模糊。 如果错,会错在: 如果工具结果都用标签明确包起来(它确实包了),模型能分清哪些是真用户说的,那这个担心不成立。