跳到主要内容

03 · 上下文管理与容错

本章讲长程 agent 的两个生死问题:上下文会越滚越大直到爆窗口;工具会(超时、空返回、格式错)。看这套代码怎么在不训练、不改模型的前提下,靠工程手段把长跑撑住。

3.1 问题:上下文只增不减

每一轮都往 messages 追加「模型输出 + 工具观察」,而工具观察(尤其网页摘要)动辄几千 token。跑几十轮,上下文必然逼近模型的 128K 窗口。撑爆了会直接报错或胡言乱语。

3.2 对策:超阈值就「强制收尾」

核心在 react_agent.py:186-209。每轮结束都数一次 token,超过 110K(110 * 1024)就触发一次「最后通牒」:

# react_agent.py:186-193(节选)
max_tokens = 110 * 1024
token_count = self.count_tokens(messages)
if token_count > max_tokens:
messages[-1]['content'] = (
"You have now reached the maximum context length ... "
"You should stop making tool calls and ... provide ... "
"<think>...</think>\n<answer>your answer</answer>"
)
content = self.call_server(messages, planning_port) # 逼它现在就答

巧妙处: 它不是粗暴截断历史,而是改写最后一条消息成一句强指令,再逼模型立刻用现有信息给 <answer>。这样答案仍基于完整证据,只是不再允许继续搜。收尾后按有没有 <answer> 标记 termination 为「因 token 上限而生成答案」或「格式错」(react_agent.py:196-201)。

为什么阈值是 110K 而非 128K?留了约 18K 余量给「最后一次回答」本身要占的输入+输出——这是个务实的安全边际。

3.3 三道时间/轮数闸门

除了上下文,还有三处硬闸,防止单题无限跑:

闸门阈值行为代码
轮数100 次 LLM 调用用完就把末条改成「超调用上限」并收尾react_agent.py:29183-184
单题墙钟150 分钟直接返回「2h30min 未找到答案」react_agent.py:140-150
visit 内部多 URL 累计 900 秒剩余 URL 返回兜底文本tool_visit.py:83-94

另外 run_multi_react.py:192-207任务层还兜了个 TimeoutError(>1800s),把整题标成 [Failed] 写盘,保证一道题卡死不会拖垮整批。

3.4 处处重试,但退避策略各异

这套代码对「外部依赖会抖」有充分预期,几乎每个网络调用都包了重试。但风格不统一,值得对照看:

位置重试次数退避代码
调主模型10指数退避 + 抖动,封顶 30sreact_agent.py:71-108
Serper 搜索5无退避,直接重连tool_search.py:63-72
Jina 抓页(内层)3固定 sleep 0.5stool_visit.py:146-166
Jina 抓页(外层)8无退避tool_visit.py:169-177
摘要 LLM可配(默认 1)tool_visit.py:107-129
沙箱执行8无退避,随机换端点tool_python.py:76-109

只有调主模型用了正经的指数退避(react_agent.py:101-106):base * 2**attempt + rand(0,1),封顶 30 秒。其余多是「硬重试」——反映出这些外部服务(Serper/Jina/沙箱)通常是瞬时抖动,重连即可。

3.5 降级:失败也要给出「能用」的东西

最能体现「长跑韧性」的是 visit 的多级降级(tool_visit.py:202-253),它的哲学是宁可给次优结果,也不让整轮崩:

怎么读这张图:从上往下依次降级,任一级成功即停:

摘要成功? ──是──► 返回 evidence + summary
│否
内容截到 0.7× 再摘要(重复 3 次) ──成功──► 返回
│仍失败
砍到 25000 字最后一搏 ──成功──► 返回
│仍失败
JSON 解析重试 3 次 ──成功──► 返回
│仍失败
返回兜底句「网页无法处理,无信息可用」(而非抛异常)

关键:兜底返回的是一段格式合法的文本(tool_visit.py:234-237251-253),主循环把它当普通观察回填,模型看到「这页读不了」后可以改去搜别的——失败被转化成了信息,而不是让循环挂掉。同样的哲学也见于工具派发:找不到工具返回错误字符串而非抛异常(见 02 §3.5)。

3.6 关键细节 / 坑

  • Python 工具的错误信息里写「5」但实际试 8 次。 tool_python.py:76 循环 range(8),但打印和判断里写的是 Attempt .../5if attempt == 4(tool_python.py:80,98,106)——即真正的提前返回发生在第 5 次,后 3 次是死代码。功能上仍是「最多 5 次有效重试」,但注释/日志有误导。

  • 数 token 每轮重载 tokenizer。01 §3.7,count_tokens 每次 from_pretrained,长轨迹下是明显开销。

  • 摘要 LLM 默认只重试 1 次。 VISIT_SERVER_MAX_RETRIES 默认 1(tool_visit.py:192),靠外层「截断再摘要」的降级兜住,而不是靠重试。

代码地图

主题文件路径符号名
上下文超限强制收尾inference/react_agent.pyMultiTurnReactAgent._run(max_tokens = 110*1024 处)
主模型指数退避inference/react_agent.pyMultiTurnReactAgent.call_server
单题墙钟闸inference/react_agent.py_run(150 * 60 处)
任务级超时兜底inference/run_multi_react.pyas_completed 循环中的 TimeoutError 分支
visit 多级降级inference/tool_visit.pyVisit.readpage_jina
网页文本截断inference/tool_visit.pytruncate_to_tokens
沙箱随机端点重试inference/tool_python.pyPythonInterpreter.call