跳到主要内容

核心循环:基于 tool-call 的多轮推理

30 秒导读: 一个 agent 之所以能"自己干活",靠的不是一次问答,而是一个循环——它先让大模型 "想一步",模型可能说"我要调某个工具",程序就去执行工具、把结果塞回对话,然后再问模型。 这样一圈一圈转,直到模型说"我不需要工具了,这是最终答复"。本章把 CowAgent 的这个循环,从 一段十行的示意代码,一路讲到它真实的实现 AgentStreamExecutor

本章是整套 CowAgent 讲解的主干。其它机制——系统提示词怎么组装长对话怎么不崩记忆与知识技能与工具自进化——都是挂在这根主干上的枝叶。 读懂了这一章,你就抓住了"一个 Agent Harness 到底怎么转"。


1. 这是什么(零基础也能懂)

先想清楚:为什么需要"循环"

单独一个大模型(LLM)只会做一件事:给它一段文字,它续一段文字。它没有手脚——不能真的 读文件、跑命令、查数据库。

那 CowAgent 这种"能帮你干活的 AI 助手"是怎么做到的?答案是给模型配一批工具(读文件、 执行 bash、搜网页……),然后在模型和工具之间架一个循环:

  • 模型不直接干活,它只会"帮我调用 bash,命令是 ls"(这叫一次 tool call,工具调用)。
  • 外面的程序(这就是本章的主角)接住这句话,真的去执行 ls,拿到输出。
  • 程序把输出塞回对话历史,再问一遍模型:"工具结果是这个,接下来呢?"
  • 模型看到结果,可能继续要下一个工具,也可能说"够了,这是给用户的答复"。

这个"想一步 → 调工具 → 看结果 → 再想"的往复,就是 agent loop(智能体循环),也叫 基于 tool-call 的多轮推理。它是所有能自主干活的 AI agent 的心跳。

一句话直觉

把 LLM 想成一个很聪明但被绑住手脚的大脑;agent loop 就是它的脊髓与四肢—— 大脑发指令(tool call),四肢执行、把感觉(tool result)传回大脑,如此反复,直到任务完成。

用起来什么样

在 CowAgent 里,发起这样一次循环只是一个方法调用(agent/protocol/agent.py:383 run_stream):

# 示意:真实签名见 Agent.run_stream
response = agent.run_stream("把当前目录下所有 .log 文件删掉")
# 内部会自动转很多圈:
# 模型 → 我要 bash("ls *.log") → 执行 → 结果回灌
# 模型 → 我要 bash("rm a.log …") → 执行 → 结果回灌
# 模型 → "已删除 3 个日志文件。" ← 不再要工具,循环结束
print(response) # "已删除 3 个日志文件。"

调用方只看到一句话进、一句话出;中间转了几圈、调了哪些工具,全被这个循环藏在了内部。


2. 顶层全景(它大概怎么转)

CowAgent 把这套循环拆成两层,各管一摊:

部件职责一句话在哪
Agent外壳:持有系统提示词、工具、线程安全的消息历史;每次调用现搭一个执行器agent/protocol/agent.py:13
AgentStreamExecutor引擎:真正跑 while 循环、调 LLM、执行工具、回灌结果agent/protocol/agent_stream.py:80
BaseTool工具契约:每个工具长什么样、怎么被调用、返回什么agent/tools/base_tool.py:30

为什么要分两层?因为外壳是长命的、引擎是一次性的Agent 对象常驻,攒着跨轮对话的记忆; 每来一条用户消息,就临时 new 一个 AgentStreamExecutor 把这轮跑完,然后把更新后的消息历史 同步回外壳。这样并发和状态就干净了。

一张图:一次 run_stream 的数据流

先说怎么读这张图:从上到下是一次调用的时间顺序;中间那个方框内部会循环多次 (虚线箭头就是"再问一遍"的回灌)。

用户消息 "帮我干 X"


┌──────────────────────┐
│ Agent.run_stream │ 外壳:现搭系统提示词、拷贝消息历史、
│ (agent.py) │ new 一个执行器、跑完再同步回来
└──────────┬───────────┘
│ 委托

┌───────────────────────────────────────────────┐
│ AgentStreamExecutor.run_stream ← 心跳在这 │
│ │
│ turn=1,2,3…(上限 max_turns) │
│ ┌──────────────────────────────────────┐ │
│ │ ① 问 LLM(流式)_call_llm_stream │ │
│ │ → 拿到 文字回复 + tool_calls │ │
│ ├──────────────────────────────────────┤ │
│ │ 有 tool_calls? │ │
│ │ 否 → 跳出循环,这段文字就是最终答复 │ │
│ │ 是 → ② 逐个执行 _execute_tool │ │
│ │ ③ 把 tool_result 追加进 messages│ │
│ └───────────────┬──────────────────────┘ │
│ └───── 再转一圈(回到 ①)┄┄┄┘ │
└───────────────────────────────────────────────┘
│ 返回最终文字

response

主线走一遍(高层,不进代码):用户消息 → 外壳搭好上下文、拷一份消息历史 → 引擎进 while 循环 → 每圈"问模型 / 执行工具 / 回灌结果" → 模型不再要工具时跳出 → 文字答复层层返回给调用方。


3. 核心原理:先用示意代码建立直觉

在钻进真实源码前,先把这个循环的骨架用十几行演出来。看懂这段,后面的真实实现就只是"给骨架 加了一堆健壮性血肉"。

这段在演示:循环的最小内核——问、判断、执行、回灌、再问。

# 示意,非源码:agent loop 的最小内核
def run_loop(user_message, tools, max_turns=50):
messages = [{"role": "user", "content": user_message}]
for turn in range(max_turns): # 有上限,防止无限打转
reply, tool_calls = ask_llm(messages) # ① 让模型想一步
messages.append(assistant_msg(reply, tool_calls))

if not tool_calls: # ② 模型不要工具了 → 收工
return reply # 这段文字就是最终答复

for call in tool_calls: # ③ 模型要工具 → 逐个真的执行
result = tools[call.name].run(call.args)
messages.append(tool_result_msg(call.id, result)) # ④ 结果回灌进历史
# ⑤ for 循环回到顶部,带着新结果"再问一遍"
return "达到步数上限,强制收尾" # 兜底

重点看四件事,它们在真实实现里会被逐一放大:

  1. for turn in range(max_turns) —— 循环必须有硬上限,否则模型可能永远要工具、转不停。
  2. if not tool_calls: return —— 循环的唯一正常出口是"模型这一步没要任何工具"。
  3. messages.append(tool_result_msg(...)) —— 工具结果必须塞回同一条消息历史,模型下一步才 看得见。这个"回灌"是整个循环的命脉。
  4. tool_calls 是模型说出来的,不是程序猜的 —— 调哪个工具、传什么参数,全由模型在 ① 里决定。

真实的 AgentStreamExecutor 就是这个骨架,外加:流式输出、失败重试、空回复兜底、上限后强制总结、 JSON 修复、取消处理。下面逐段看。


4. 真实实现:run_stream 主循环

真源码在 agent/protocol/agent_stream.py:340 AgentStreamExecutor.run_stream。它做的第一件事是把 用户消息以 Claude 的 content-blocks 格式追加进历史(注意 content 是个列表,不是裸字符串):

# agent_stream.py:360 run_stream —— 用户消息入历史
self.messages.append({
"role": "user",
"content": [{"type": "text", "text": user_message}],
})

紧接着是进循环前做一次上下文修剪agent_stream.py:373),而不是在每步做——这是刻意的: 修剪若发生在循环中途,可能把当前这轮刚生成的 tool_use/tool_result 链切断,导致模型犯迷糊。 修剪与健壮性是第 3 章的主题,这里只需知道"修剪只在开跑前做一次"。

4.1 循环骨架与 turn 计数

主体就是一个 while turn < self.max_turns 循环(agent_stream.py:393)。每圈开头先探一次"用户 是否点了取消",然后 turn += 1、发一个 turn_start 事件,再调 LLM:

# agent_stream.py:393 run_stream 主循环骨架(节选)
while turn < self.max_turns:
self._check_cancelled() # 取消点:turn 边界
turn += 1
assistant_msg, tool_calls = self._call_llm_stream(retry_on_empty=True) # ① 问模型
final_response = assistant_msg

if not tool_calls: # ② 没要工具 → 准备收工(含空回复处理)
...
break

# ③ 有工具 → 逐个执行、回灌(见 4.2)

对照第 3 节的示意骨架,while + turn + if not tool_calls: break 一一对应。max_turns 由外壳 传入(Agent.max_steps,默认 100,见 agent.py:43),是防止无限循环的第一道闸

4.2 执行工具、回灌结果

拿到 tool_calls 后,程序逐个执行,把每个结果打包成一个 tool_result block,最后一次性作为 一条 user 消息追加进历史(agent_stream.py:586):

# agent_stream.py:571 为每个工具结果构造 tool_result block
tool_result_block = {
"type": "tool_result",
"tool_use_id": tool_call["id"], # 必须对上前面 assistant 的 tool_use.id
"content": result_content,
}
if is_error:
tool_result_block["is_error"] = True # 让模型明确知道这步失败了

这里有个极其关键的健壮性设计:追加 tool_result 的代码放在 try/finallyfinally 里 (agent_stream.py:583)。含义是——哪怕工具执行途中抛异常,也一定要把 tool_result 补上。 为什么?因为 Claude / OpenAI 的消息格式强制要求:每个 tool_use 都必须有一个配对的 tool_result,否则下一轮请求直接被 API 拒绝。少一个配对,整个对话就废了。所以哪怕是紧急兜底, 也要塞一个"Tool execution was interrupted"的错误结果进去(agent_stream.py:619)。

一句话记住: tool_usetool_result 必须成对出现,这是这套消息格式的铁律;finally 就是为了在任何异常路径下都守住这个不变量。

结果回灌后,while 回到顶部,带着新的 tool_result 再问一遍模型——循环闭合。

4.3 三种"非正常"收尾

真实循环比示意骨架多了三种边界处理,都是实战踩出来的:

情况触发条件怎么处理源码
空回复模型既没文字也没工具调用(工具跑完后常见)若非首轮,注入一句"请向用户说明结果"再问一次;仍空则给固定兜底文案agent_stream.py:409
到达步数上限turn 撞到 max_turns注入一句"你已达步数上限,别再调工具,直接总结",逼模型出一段文字收尾agent_stream.py:642
用户取消cancel_event 被置位AgentCancelledError_handle_cancelled 补齐悬空的 tool_result、加"已取消"标记agent_stream.py:687

注意"空回复"和"到达上限"这两处都用了同一个小技巧:临时注入一条 user 提示 → 调一次 LLM → 再把这条注入消息从历史里删掉agent_stream.py:437:682),这样它只用来"催"模型一下, 不会污染最终持久化的对话记录。


5. _call_llm_stream:流式解析与"混合格式"

第 4 节的 ① 问模型 那一步,展开就是 agent_stream.py:775 _call_llm_stream。这是本章第二个重点, 它要理解 CowAgent 一个很有意思的设计——输入用 OpenAI 风格的流,存储用 Claude 风格的消息

5.1 一边流式收,一边攒三样东西

模型的回复是逐块(chunk)流式吐出来的。_call_llm_stream 迭代这个流,同时往三个桶里攒 (agent_stream.py:857):

  • full_content —— 给用户看的正文文字
  • full_reasoning —— 模型的思维链 / reasoning(若开了深度思考)
  • tool_calls_buffer —— 工具调用(分块拼接,见下)

每收到一块,就实时发 message_update 事件推给前端(agent_stream.py:954),所以用户能看到 文字一个字一个字冒出来。

5.2 tool_calls 是"攒"出来的(OpenAI 风格流)

这里是关键细节:模型要调工具时,工具名和参数不是一次给全的,而是拆成很多小块流过来,程序 要按 index 把它们拼起来(agent_stream.py:957):

# agent_stream.py:957 逐块拼接 tool_calls(OpenAI delta 风格)
for tc_delta in delta["tool_calls"]:
index = tc_delta.get("index", 0)
if index not in tool_calls_buffer:
tool_calls_buffer[index] = {"id": "", "name": "", "arguments": ""}
if tc_delta.get("id"):
tool_calls_buffer[index]["id"] = tc_delta["id"]
if "function" in tc_delta:
func = tc_delta["function"]
if func.get("name"):
tool_calls_buffer[index]["name"] = func["name"]
if func.get("arguments"):
tool_calls_buffer[index]["arguments"] += func["arguments"] # 字符串一段段拼

注意 arguments字符串累加+=)——参数的 JSON 是一小段一小段拼出来的,流结束后才是一个 完整的 JSON 字符串,届时再解析(见 5.4)。这套 choices[0].delta.tool_calls[].function.arguments 的结构,是 OpenAI 的 chat completions 流式协议

5.3 存进历史时,翻成 Claude 的 content-blocks

流读完后,程序把这一轮 assistant 的回复组装成 Claude 风格的 content-blocks 列表再入历史 (agent_stream.py:1144):一个 thinking block(思维链,会先截断,见下)+ 一个 text block + 若干 tool_use block:

# agent_stream.py:1164 把 OpenAI 风格的 tool_calls 转成 Claude 的 tool_use block
for tc in tool_calls:
assistant_msg["content"].append({
"type": "tool_use",
"id": tc.get("id", ""),
"name": tc.get("name", ""),
"input": tc.get("arguments", {}),
})

这就是"混合格式"的全貌:

接收(读流) 存储 / 回放(消息历史)
OpenAI 风格 ──► Claude 风格 content-blocks
choices[].delta: {role, content: [
.content {type:"thinking", ...},
.reasoning_content {type:"text", ...},
.tool_calls[].function {type:"tool_use", id,name,input},
{type:"tool_result", tool_use_id, content} ← 工具结果
]}

为什么这么设计?因为 CowAgent 要同时对接一堆不同厂商的模型(Claude、GPT、DeepSeek、Gemini、 MiniMax……),底层适配器统一吐 OpenAI 风格的流最省事;而 Claude 的 content-blocks 表达力更强 (能把 thinking / text / tool_use 放在同一条消息里、tool_result 用 id 精确配对),更适合做 存储和多轮回放。于是就有了"进来是 OpenAI 流、存下去是 Claude blocks"的组合。

几个厂商专属的小尾巴也在这里处理:Gemini 的 thoughtSignature 要原样透传给下一轮 (_gemini_raw_partsagent_stream.py:978:1174);MiniMax 把 <think> 直接塞正文里,要按渠道 决定是渲染还是剥掉(_filter_think_tagsagent_stream.py:240)。这些是"让混合格式在真实厂商上跑 得通"的胶水。

5.4 参数解析:JSON + json_repair + 截断识别

拼好的 arguments 字符串要变成真正的 dict,交给 _parse_tool_argsagent_stream.py:56)。它按 三级降级处理,因为模型吐的 JSON 不总是合法的:

arguments 字符串


json.loads() ── 成功 ─► 返回 dict
│失败(JSONDecodeError)

是被 max_tokens 截断的吗? ← finish_reason 是 length/max_tokens,
│是 或字符串结尾不是 "}"

直接报"输出被截断,请拆小分多次调用"(不修,修也没用)
│否

json_repair 尝试修复(转义错乱等)── 成功 ─► 返回修好的 dict
│仍失败

返回原始解码错误

真源码里这三级一目了然:

# agent_stream.py:66 _parse_tool_args 的降级判断(节选)
except json.JSONDecodeError as e:
if finish_reason in ("length", "max_tokens") or not args_str.rstrip().endswith("}"):
return {}, "Output truncated (max_tokens reached). Split content into smaller chunks…"
if _HAS_JSON_REPAIR:
repaired = _repair_json(args_str, return_objects=True) # 容错修复
if isinstance(repaired, dict):
return repaired, None
return {}, f"Invalid JSON in tool arguments: {e.msg}"

先判截断、再修复这个顺序很讲究:如果 JSON 是被 token 上限硬切断的(比如模型在写一个超长的 文件内容写到一半没词了),json_repair 就算"修好"也会丢掉后半段内容、悄悄给出错误结果——那还 不如直接告诉模型"你被截断了,把内容拆小分几次调用"。解析失败的工具调用会带上 _parse_error 标记继续往下走,在 _execute_tool 里被转成一条 error 结果(agent_stream.py:1202),依旧保住 tool_use/tool_result 的配对。


6. _execute_tool:一次工具执行的生命周期

第 4.2 节里"逐个执行"那一步,展开是 agent_stream.py:1188 _execute_tool。它把一次工具调用 包成一个有头有尾的小流程:

  1. 解析错误短路:若这个 call 带 _parse_error(5.4 里没解析成功的),直接返回 error,不执行。
  2. 连续失败保护:调 _check_consecutive_failuresagent_stream.py:269)——这是防打转的第二道闸, 下面单独讲。
  3. 取工具、设上下文:从 self.tools 字典按名取工具,给它挂上 modelcontext(就是 Agent 自己)、以及一个 progress_callback(工具可以借它往前端推进度)(agent_stream.py:1244)。
  4. 计时执行tool.execute_tool(arguments),记 execution_time
  5. 记账_record_tool_result(工具名, 参数hash, 成功与否) 存进 tool_failure_historyagent_stream.py:332),供第 2 步的失败保护查历史。
# agent_stream.py:1256 执行工具并计时(节选)
start_time = time.time()
try:
result: ToolResult = tool.execute_tool(arguments) # 真正干活
finally:
tool.progress_callback = None # 用完摘掉回调
execution_time = time.time() - start_time
success = result.status == "success"
self._record_tool_result(tool_name, arguments, success) # 记账,供防打转用

防打转:不止 max_turns 一道闸

max_turns 拦的是"总步数",但真实里更常见的坏味道是模型拿同样的参数反复调同一个工具(陷在 局部)。_check_consecutive_failures 就盯这个,用 tool_failure_history 做了多级熔断agent_stream.py:269):

触发条件动作
同工具 + 同参数连续调 5 次(不管成没成功)停这次调用,提示"疑似死循环,结果之前已返回"
同工具 + 同参数连续失败 3 次停,提示换个完全不同的方法
同工具(任意参数)连续失败 6 次
同工具连续失败 8 次致命critical_error,直接中止整个对话并给用户道歉文案

这些数字(5/3/6/8)都是硬编码的经验阈值。它们和 max_turns 一起,构成 CowAgent 对"agent 卡死" 的纵深防御。


7. 工具契约:BaseTool

循环能"调工具",前提是所有工具长一个统一的样子。这个契约就是 agent/tools/base_tool.py:30 BaseTool。它很薄,但每个字段循环都要用到:

成员作用源码
name / description / params工具的名字、说明、参数 JSON Schema——组装给 LLM 的工具定义时要用base_tool.py:37
execute_tool(params)循环调用的统一入口;内部 try 包一层再转发到 executebase_tool.py:61
execute(params)子类真正实现干活逻辑的地方(抽象,必须重写)base_tool.py:67
stageToolStage工具属于哪个阶段:PRE_PROCESS(模型主动调)还是 POST_PROCESS(收尾自动跑)base_tool.py:7

工具执行结果统一用 ToolResultbase_tool.py:13)包装,只有两种造法:

# base_tool.py:21 工具结果的两个静态构造器
ToolResult.success(result) # status="success"
ToolResult.fail(result) # status="error"

execute_tool 返回的 result.status 就是第 6 节里 success = result.status == "success" 判断成败 的依据,一路上溯决定 tool_result block 要不要带 is_error

ToolStage:两类工具,两种时机

ToolStagebase_tool.py:7)把工具分成两拨:

  • PRE_PROCESS(默认) —— 循环里由模型主动选择调用的工具(bash、读文件等)。本章讲的 循环全程操作的都是这类。
  • POST_PROCESS —— 不进循环,在整轮 run_stream 结束后由外壳统一跑一遍的收尾工具,见 Agent._execute_post_process_toolsagent.py:312)。它们不吃参数,自己从 context(Agent)里 取需要的信息,典型用途是"这一轮结束后顺手做点副作用"。

8. 外壳:Agent 类做了什么

引擎(AgentStreamExecutor)是一次性的;长命的状态都在外壳 Agentagent/protocol/agent.py:13)。 本章只需理解外壳与循环相关的三件事。

8.1 run_stream 外壳:搭台、委托、同步回来

Agent.run_streamagent.py:383)本身不跑循环,它只做三件事:

# agent.py:428 Agent.run_stream 的三步(节选)
full_system_prompt = self.get_full_system_prompt(skill_filter=skill_filter) # ① 现搭系统提示词
with self.messages_lock:
messages_copy = self.messages.copy() # ② 拷一份消息历史给引擎
executor = AgentStreamExecutor(agent=self, ..., messages=messages_copy, max_turns=self.max_steps)
response = executor.run_stream(user_message) # 委托引擎跑完整个循环
with self.messages_lock:
self.messages = list(executor.messages) # ③ 把引擎更新后的历史同步回外壳
  • ① 系统提示词每次现搭get_full_system_promptagent.py:106)每轮从磁盘重读 AGENT.md / USER.md / RULE.md、刷新技能与工具再拼——这是第 2 章的主题,本章不展开, 只需知道传给引擎的 system_prompt 是"每轮从零重建"的。
  • ② 传的是消息历史的拷贝:引擎在拷贝上折腾(可能修剪、可能清空),跑完再由外壳决定怎么并回。
  • ③ 用替换而非追加同步:因为引擎可能修剪掉了历史,长度会变短,所以是 self.messages = list(...) 整体替换(agent.py:470),不能简单 append。

8.2 messages / messages_lock:线程安全

外壳的 self.messages 是跨轮对话的唯一真相,可能被多线程访问(一个用户消息在跑、另一个渠道又 来一条)。所以所有读写都用 self.messages_lock 这把锁围起来(agent.py:50:432:469)。引擎在 自己的拷贝上工作、外壳在锁内并回,这套"拷贝—委托—并回"就是 CowAgent 处理并发的办法。

8.3 异常时的自愈

如果引擎中途因上下文溢出 / 消息格式错误把自己的 messages 清空了(自救,见 第 3 章),外壳会察觉到并把自己的历史也清掉(agent.py:460), 免得下一条消息又撞进同一个溢出——这是外壳和引擎之间的一个默契。


9. 边界与局限(本章刻意不覆盖的)

这一章只讲主循环这根主干,几个紧邻的机制留给兄弟章节,别在这里找:

想了解去哪
系统提示词怎么每轮从零重建、AGENT.md/USER.md/RULE.md 怎么拼02-system-prompt.md
_trim_messages / _aggressive_trim_for_overflow / 溢出自愈 / 消息合法性修复03-context-robustness.md
三层记忆、flush_memory、上下文摘要注入04-memory-knowledge.md
具体有哪些工具、SKILL.md 渐进式披露、MCP 工具检索(_select_tools_for_injection)内部05-skills-tools-mcp.md
空闲时回看对话、自我进化06-self-evolution.md

另外几个诚实的边界:

  • 循环是同步、串行的:一轮里多个 tool_calls依次执行(agent_stream.py:502 的 for),没有 并行工具调用。
  • max_turns 默认 100agent.py:43),撞上限后是"强制总结"而非报错,所以超长任务可能拿到一个 "没做完"的收尾,而不是完整结果。
  • token 估算是近似的_estimate_text_tokensagent.py:270)用"CJK≈1.5、ASCII≈0.25 字/token" 的粗估,不是真实分词,所以修剪触发的时机是估出来的、有偏差。

10. 巧妙之处(可带走的技术)

  1. finally 里补 tool_resultagent_stream.py:583):把"守住 tool_use/tool_result 配对"这个 API 铁律放进 finally,任何异常路径都塌不了消息历史。这是做多轮 tool-call 循环最该抄的一招。
  2. 注入式提示后即删agent_stream.py:437:682):临时插一条 user 消息催模型,用完从历史里 删掉——既能"引导"模型,又不污染持久化记录。
  3. 先判截断、再修 JSONagent_stream.py:67):识别 max_tokens 截断后拒绝用 json_repair "修",避免悄悄丢内容——诚实报错胜过假装成功。
  4. 多级失败熔断agent_stream.py:269):在 max_turns 之外,用参数 hash 历史盯"同参重复调用", 把"卡死"和"报错"分开处理,最后 8 次失败给用户一段体面的道歉而非崩溃。
  5. 混合格式(§5.3):进来用 OpenAI 流(好适配多厂商)、存下去用 Claude content-blocks(表达力 强、好回放),各取所长。

11. 代码地图(导航索引)

用符号名 grep 比记行号更抗漂移。

主题文件路径符号
循环外壳、消息历史、并发同步agent/protocol/agent.pyAgent.run_stream
POST_PROCESS 收尾工具agent/protocol/agent.pyAgent._execute_post_process_tools
token 估算(触发修剪用)agent/protocol/agent.pyAgent._estimate_text_tokens
主循环 / turn 计数 / 上限总结 / 空回复兜底agent/protocol/agent_stream.pyAgentStreamExecutor.run_stream
流式解析、混合格式、组装 assistant 消息agent/protocol/agent_stream.pyAgentStreamExecutor._call_llm_stream
工具参数三级解析(json→repair→截断)agent/protocol/agent_stream.py_parse_tool_args
单次工具执行、上下文注入、计时agent/protocol/agent_stream.pyAgentStreamExecutor._execute_tool
多级失败熔断(防打转)agent/protocol/agent_stream.pyAgentStreamExecutor._check_consecutive_failures
取消处理、补齐悬空 tool_resultagent/protocol/agent_stream.pyAgentStreamExecutor._handle_cancelled
思维链存储截断agent/protocol/agent_stream.py_truncate_reasoning_for_storage
工具契约、执行入口agent/tools/base_tool.pyBaseTool.execute_tool
工具阶段枚举agent/tools/base_tool.pyToolStage
工具结果包装agent/tools/base_tool.pyToolResult.success / ToolResult.fail