InferenceStage 的运行流程:推理与工具执行循环

echo-agent 前身为 2025 年 11 月启动的个人助理项目 fubot,最初面向长期陪伴型个人智能体,围绕认知记忆、上下文延续、用户偏好沉淀、任务闭环与持续自我优化展开。随着真实场景迭代,项目逐步形成多入口接入、统一事件模型、消息总线、Agent Loop、多模型抽象、工具调用、MCP 接入、任务调度、权限审批、运行轨迹、长期记忆和受控自演进等能力。目前已支持微信、QQ、CLI、Gateway、Webhook、Cron 等入口,服务用户超过 20 万、累计下载超过 50 万,是面向长期运行、记忆增强和可持续成长智能体的开源 Agent Runtime。

项目地址:github.com/fuyuxiang/e...

你让 Agent "帮我修复测试失败"。一个普通聊天模型可能会直接猜原因,或者给你一套通用排查清单。

但一个真正能工作的 Agent,应该先看仓库、读日志、运行测试、判断失败点,必要时修改代码,再验证结果。这里的关键不是"模型会不会说出工具名",而是模型、工具、权限、观察和终止条件能不能组成一条稳定闭环。

本篇只讲一个点:InferenceStage 的职责不是调用模型,而是把模型输出的行动提案转化为受控、可回写、可终止、可审计的推理循环。

问题入口

很多最小 Agent demo 会写成这样:

ini 复制代码
response = llm.chat(messages, tools=tool_defs)
if response.tool_calls:
    result = execute_tool(response.tool_calls[0])
    messages.append(result)
    response = llm.chat(messages, tools=tool_defs)

这段代码能解释 tool calling 的基本形式,但离生产级 Agent 还很远。

真实系统里,模型可能连续调多个工具;某个工具可能持续失败;工具参数可能越权;写文件、执行命令、发送消息这类动作可能产生副作用;工具输出可能很长;模型可能陷入同参重复调用;provider 也可能在流式输出到一半时失败。

如果这些问题都靠"提示词里让模型谨慎一点"解决,Agent 的安全边界就退化成了模型自觉。

会调用工具只说明有行动接口;能否算 Agent,要看工具调用是否进入闭环、是否受权限约束、是否能被追踪和复盘。

为了不停留在抽象层面,下面以 echo-agent 的实现为例。上一阶段 ContextStage 已经把消息、工具定义、任务类型、计划和流式发布器装进 PipelineContextInferenceStage 接过的不是零散参数,而是一份已经构造好的推理快照。

它最终返回 InferenceResult:最终文本、工具调用次数,以及是否建议响应阶段触发技能或记忆复盘。它不负责保存最终回答,也不负责后台整理。InferenceStage 做推理与行动,ResponseStage 做收尾。

Agent Loop

推理循环的基本结构并不复杂。每一轮先根据熔断状态过滤工具,再调用模型;如果模型没有工具调用,就结束;如果模型请求工具,就执行工具,把结果作为 tool 消息写回上下文,然后进入下一轮。

可以把它压成一段伪代码:

ini 复制代码
async def run(ctx):
    messages = ctx.messages
    response_text = ""
    repeat_tracker = {}
​
    for iteration in range(max_iterations):
        unavailable = circuit_breaker.get_unavailable_tools()
        tools = filter_out(ctx.tool_defs, unavailable)
​
        response, route = await chat_stream_with_routing(
            messages=messages,
            tools=tools,
            on_delta=ctx.stream_publisher.on_delta,
        )
​
        issues = inference.validate_response(response)
        trace_llm(iteration, route, response.finish_reason, issues)
​
        if response.finish_reason == "error":
            return fallback_or_last_text(response_text)
​
        if response.content:
            response_text = response.content
​
        if not response.has_tool_calls:
            mark_plan_complete_if_needed(ctx)
            return response_text
​
        append_assistant_tool_calls(messages, ctx.session, response)
​
        for tool_call in response.tool_calls:
            result = await run_tool_with_guards(ctx, tool_call, repeat_tracker)
            append_tool_result(messages, ctx.session, tool_call, result)
​
    return fallback_or_last_text(response_text)

这段伪代码的重点不是函数名,而是控制权位置:模型只提出下一步,循环由系统推进。模型说"我要调用工具",并不等于系统已经决定执行工具。

这就是 ReAct 思想在工程里的落地:模型产生行动意图,系统执行行动,行动结果回到模型上下文,模型再继续推理。不同的是,生产实现必须在每个箭头上加边界。

行动提案

Agent 架构里必须区分"模型输出"和"系统命令"。tool call 更准确地说是行动提案:模型根据当前上下文判断某个工具可能有助于完成任务,但这个提案还要经过系统校验。

如果把模型输出直接当命令执行,Prompt Injection、误解用户意图、错误参数和危险副作用都会被放大。专业系统要在模型之后建立独立决策层。

echo-agent 的转换链路是:

环节 职责
模型 基于上下文提出工具调用
InferenceController 校验响应约束,如工具白名单、黑名单、JSON 输出要求
ApprovalGate 判断本次工具调用是否允许执行
ToolRegistry 参数校验、超时、重试、replay 防护和审计
messages/session 保存 assistant 工具请求与 tool 结果
下一轮模型 读取观察,决定继续行动或生成最终回答

这里有一个容易忽略的细节:模型响应校验不是都强制失败。当前实现发现问题后主要记录 warning,例如要求必须调工具但模型没调、调用了 blocked tool、要求 JSON 却返回非 JSON。这是一种温和约束,先提供可观测信号,再由更严格场景决定是否重试、修复格式或直接失败。

工具事务

从工程角度看,每一次工具调用都像一个小事务。它有输入、权限检查、执行上下文、结果、日志,以及可能的副作用。

只读工具风险较低,但仍可能泄露敏感信息或造成高成本访问。写入工具会改变文件、记忆、任务或技能,需要路径和权限约束。执行类工具可能启动进程、访问网络、修改系统状态,需要更强审批。外部可见动作,例如发送消息、安装技能、创建调度任务或调用不可信 MCP 工具,更需要审计和人工确认。

所以 echo-agent 不会在模型请求工具后直接执行,而是先进入 ApprovalGate。审批会综合静态 guard、权限策略、风险分类、通道信任、自动批准、approval mode、allowlist、unattended 模式、smart approval 和 manual approval flow。

如果审批被拒绝,系统不会把它当异常炸掉,而是把拒绝信息作为 tool 结果写回上下文。模型下一轮能看到"这个动作被拒绝了",然后解释原因、调整方案,或者请求用户确认。

审批通过后,系统构造 ToolExecutionContext。这里面包括 execution_idtrace_idsession_keyuser_idcredentialsapproved_actionsallowed_tools,以及一个关键字段:idempotency_key

幂等键用于阻止副作用工具被重复执行。构造方式大致是把 trace_id、工具名、工具序号和排序后的参数哈希到一起:

python 复制代码
def build_idempotency_key(trace_id, tool_name, index, params):
    payload = json.dumps(params, sort_keys=True, ensure_ascii=False)
    digest = sha256(f"{trace_id}:{tool_name}:{index}:{payload}".encode())
    return digest.hexdigest()[:24]

这不是形式主义。写文件、执行命令、发送消息、修改外部系统,重复执行一次就可能造成真实损害。幂等键让 ToolRegistry 能识别同一执行范围内的 replay,并阻止副作用重复发生。

执行内核

ToolRegistry.execute 不是一个普通字典查找。它是工具执行内核。

它会先解析工具别名,例如 bash 映射到 execrun_code 映射到 execute_code。然后检查执行上下文里的 allowed_tools,避免多 Agent 或受限 worker 越过自己的工具白名单。

接着它会确认工具存在,执行参数 schema 校验。对于副作用工具,它会检查 replay cache;真正执行时用 asyncio.wait_for 包裹,受 timeout_seconds 限制;失败后按 max_retries 重试。

执行日志还会记录工具名、脱敏参数、执行 ID、trace ID、开始与完成时间、成功状态和尝试次数。keytokensecretpasswordapi_key 这类敏感字段会被掩码。

这也是为什么工具调用应该被视为事务,而不是函数调用。函数调用关心返回值;工具事务还要关心权限、幂等、超时、重试、审计和副作用。

观察回写

工具结果不是普通文本,而是新的环境观察。模型需要根据它决定下一步,系统也需要保留它与具体工具调用之间的关系。

echo-agent 在模型返回工具调用后,会先把带有 tool_calls 的 assistant 消息加入 messages,同时写入 session。工具执行后,再追加对应的 tool 消息,并带上 tool_call_id 和工具名。

这有两层意义。

第一,许多模型 API 要求工具结果必须跟在对应的 assistant tool call 后面,否则消息结构非法。第二,会话历史能恢复完整行动轨迹:模型为什么调工具、调了哪个工具、参数是什么、工具返回了什么。

工具输出还会在写入前截断,当前上限是 16000 字符。长日志不应原样塞进上下文,否则后续推理会被噪声淹没,甚至挤掉当前目标和安全约束。若需要完整原始结果,工具应把原文写入文件、对象存储或审计日志,再把摘要和路径返回给模型。

工具结果不是回答素材的散装文本,而是带来源、带关联、带成功失败语义的环境反馈。

这能解释为什么 ToolResult 需要结构化表达。成功、失败、错误信息、数据载荷、元数据和截断策略,都会影响下一轮推理质量。观察越清楚,模型越不容易把失败当成功、把噪声当证据。

终止边界

没有终止条件的 Agent 不是更自主,而是不可控。推理阶段越强,停止机制越重要。

echo-agent 的推理循环有多层刹车。

边界 作用
max_iterations 防止模型无限自我驱动
circuit breaker 持续失败的工具暂时从可见工具列表移除
repeat tracker 同名同参数工具调用达到阈值后阻断
ApprovalGate 高风险或越权动作在执行前被拒绝
provider fallback 候选模型失败时尝试后备链
finish_reason error provider 不可恢复错误时退出并返回兜底
工具超时与重试 避免工具长时间挂起或瞬时失败直接中断

重复调用尤其值得单独看。模型连续调用同一工具、同一参数,通常说明它没有从反馈中获得新信息,或者没有理解工具结果。echo-agent 使用 _repeat_tracker 记录工具名和参数哈希;同参重复达到阈值后,会把最后一条工具消息替换成 blocked 提示,让模型知道必须换策略或停止。

工具熔断解决的是另一类问题:某个工具持续失败时,不要继续暴露给模型。模型并不知道外部 API 是否故障、命令执行器是否不可用、配置是否缺失。熔断不是删除工具,而是在恢复窗口内暂时隐藏;成功执行记录 success,失败执行记录 failure。

模型路由也有边界。启用 ModelRouter 后,系统会根据任务类型、内容和配置生成候选链,RouteDecision 包含 provider、model、fallback_chain、reason、context_window、max_tokens 和 temperature。若某个候选模型已经通过流式输出向用户发出增量,后续再失败,当前实现不会静默切到另一个模型继续输出,避免用户看到两个模型混合生成的回答。

可观测性

生产级 InferenceStage 还必须解释自己做过什么。

echo-agent 会为模型调用创建 llm_{iteration} span,为工具调用创建 tool_{iteration}_{tool_index} span。span metadata 里记录模型、provider、路由原因、finish reason、工具名和执行结果。若启用 OpenTelemetry,还会记录对应工具 span。

这样一次任务可以被拆成:

复制代码
process_message
  llm_0
  tool_0_0
  llm_1
  tool_1_0
  llm_2

当回答出错时,团队可以判断问题来自模型路由、工具失败、审批拒绝、重复调用阻断,还是最大迭代耗尽。

流式输出也被放在正确边界内。InferenceStage 只把模型 delta 交给 ctx.stream_publisher.on_delta,真正的段落刷新、句子刷新、通道发布和最终消息合并由流式发布器与后续阶段处理。工具密集型任务里,用户可以看到进度事件,例如 Using tool: exec,但不应把中间半成品误认为最终回答。

生产可用性

判断一个推理阶段是否生产可用,不能只问"能不能调工具"。更可检验的标准是:

检查项 可检验标准
行动闭环 assistant tool call 与 tool result 成对写入上下文和会话
权限治理 工具执行前经过 ApprovalGate,区分只读、写入、执行和高风险动作
副作用保护 副作用工具有幂等键、replay cache、审批记录
失败恢复 参数错误、审批拒绝、provider 错误能转为可理解状态
循环控制 有最大迭代、重复调用阻断、工具熔断和取消路径
结果治理 工具输出截断,完整结果通过文件或审计日志保留
路由解释 provider、model、fallback 原因和失败状态可追踪
用户体验 长任务有非最终进度事件,最终回答由 ResponseStage finalize
回归评估 有工具调用 trace、失败样例、循环耗尽样例和权限拒绝样例

这里的核心判断很简单:模型负责提出行动方向,系统负责判断行动能不能执行,工具负责接触环境,观察再反馈给模型。

如果缺少系统控制层,Agent 只是模型的外接手脚;如果缺少观察回写,工具只是一次性插件;如果缺少终止与审计,行动能力越强,线上风险越高。

小结

InferenceStage 是 Agent 行动能力的中心。它承接 ContextStage 构造好的世界,让模型在其中提出行动提案,再把提案交给审批、工具执行、结果回写、循环终止和可观测系统共同治理。

理解这一层后,Agent 的核心不再是"模型会不会调用工具",而是"每次工具调用是否减少了不确定性,是否遵守权限边界,是否留下可复盘轨迹,并在合适时机收敛"。

这也是从 Chatbot 到 Agent 的关键跃迁:回答不再只是一次语言生成,而是在有限成本和明确边界内,把不确定推理逐步变成可校验观察。

(全篇完)


本文为 echo-agent 设计笔记系列第 09 篇。项目源码已开源至 GitHub。如果你对工业级 Agent 的工程落地感兴趣,欢迎加入技术交流群(QQ群号:47572014)参与日常讨论。下一篇我们将探讨 《ResponseStage 设计笔记:回答落盘与后台整理》,敬请期待。

相关推荐
universeplayer2 小时前
我给 AI Agent 装了个飞机黑匣子:录下每一次 LLM 调用,崩了能确定性回放
llm·agent
Hector_zh2 小时前
实战·第八篇:当模型陷入死循环——FACA破解JSON生成的架构陷阱
人工智能·agent·vibecoding
嘻嘻仙人2 小时前
Claude Code CLI 实战案例——不同场景案例实操
agent
codedx3 小时前
LangChain 和 LangGraph 构建的 Agent 项目模版
后端·langchain·agent
小七-七牛开发者3 小时前
周一上线 | SpaceX 收购 Cursor、支付宝进入 AI 时代、DeepSeek 完成 500 亿元融资
ai·agent·token·glm·智谱·claudecode·ai coding·周一上线
葫芦和十三4 小时前
图解 MongoDB 08|ESR 原则:复合索引的字段顺序怎么定
后端·mongodb·agent
葫芦和十三11 小时前
图解 MongoDB 07|索引类型:七种索引,七种访问形状
后端·mongodb·agent
艾逗笔16 小时前
从 OpenClaw 到 FastClaw:如何设计优秀的多 Agent 架构
agent