Agent 工具系统:把模型意图变成可控能力
2026/7/1 17:53:50 网站建设 项目流程

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

项目地址:GitHub - fuyuxiang/echo-agent: Echo Agent 是一个可自托管、长期运行、持续学习的 AI Agent,面向个人与团队的私有自动化场景。它可以部署在自有服务器上,统一连接模型、工具、记忆、权限与消息入口。内置四层认知记忆、遗忘曲线与矛盾检测机制,能够在跨会话任务中持续沉淀上下文,并保持长期记忆的质量。针对命令执行、文件操作等高风险行为,它提供基于 LLM 的审批与解释机制,为关键操作建立可审计、可追溯的安全边界。原生支持 MCP、A2A、多模型路由、任务调度、工具调用和多通道接入,覆盖 CLI、Gateway API、微信、Telegram 等入口。它让 Agent 带着长期记忆和可进化技能,持续、安全地为你工作。 · GitHub

你让 Agent “帮我修复测试失败”。模型很快判断:应该读日志、搜索相关文件、运行测试、修改代码,再重新验证。

如果这只是一次聊天,模型说出这套步骤就结束了。但对 Agent 来说,真正的问题从这里才开始:它能不能读文件?能不能执行命令?能不能写代码?写到哪里?失败后能不能重试?每一步有没有记录?

工具系统要解决的不是“让模型会调用函数”,而是把模型生成的行动意图,转换成受约束、可审批、可追踪、可恢复的真实能力

问题入口

最小化的工具调用 demo 通常长这样:

response = llm.chat(messages, tools=tool_defs) if response.tool_calls: result = execute(response.tool_calls[0]) messages.append(result)

这段代码能说明 tool calling 的基本形式,但不能构成生产级工具系统。

因为模型生成的tool_call不是系统命令,而是行动提案。模型可能漏传参数、传错类型、调用不该暴露的工具、重复执行副作用动作,或者把一段含糊自然语言塞进参数里。

如果系统把这些提案直接执行,Agent 的安全边界就变成了模型自觉。工具越强,风险越大。

会调用工具只说明模型有行动接口;工具调用是否可控,要看 schema、权限、幂等、审批、超时、重试和审计是否进入执行链路。

工具边界

工具不是普通函数。

普通函数由开发者调用,调用者理解代码上下文,知道哪些参数合法、哪些副作用危险、失败后应该怎么处理。Agent 工具由模型选择,调用者是一个概率系统。它只能通过工具名、描述和 schema 推断怎样行动。

所以生产级工具至少要声明这些信息:

维度要回答的问题
名称与描述模型什么时候应该使用它
参数 schema模型必须提供哪些结构化输入
参数校验错误参数能否在执行前拦住
readiness当前环境里工具是否可用
风险级别只读、写入、执行还是危险动作
副作用是否会改变外部状态
超时与重试执行失败时系统如何收敛
执行上下文属于哪个 session、用户和 trace
返回结果如何反馈给模型进入下一轮决策

为了不停留在抽象层面,下面以 echo-agent 的实现为例。它的工具系统由ToolToolExecutionContextToolResultToolRegistrydiscover_toolsfilter_tools_by_policyApprovalGate等模块共同组成。

Tool是所有工具的抽象基类。它不仅定义execute,也定义工具名、描述、参数、超时、重试、能力标签和风险等级:

class Tool(ABC): name: str = "" description: str = "" parameters: dict[str, Any] = {} timeout_seconds: int = 30 max_retries: int = 0 stream_capable: bool = False capabilities: tuple[str, ...] = () risk_level: str = "write" ​ @abstractmethod async def execute( self, params: dict[str, Any], ctx: ToolExecutionContext | None = None, ) -> ToolResult: ...

这段抽象传递了一个关键判断:工具不是一段可调用代码,而是带声明、约束和执行现场的能力入口。

模型并不直接面对真实文件系统、Shell、网络和任务系统。它面对的是一组被系统选择后暴露出来的工具 schema。工具设计得清楚,模型更容易正确行动;工具设计得含糊,模型就会把不确定性带进执行层。

Schema 协议

工具暴露给模型时,不是把 Python 对象传给模型,而是转换成 schema。

schema 会进入模型 API 的tools参数。模型看到工具名、描述和参数结构,然后决定是否发起调用。系统也通过 schema 限制模型能传什么。

这意味着 schema 不是文档,而是模型和系统之间的协议。

如果写文件工具只有一个instruction: string,模型可能传入“帮我把配置修好,如果需要就覆盖旧文件”。这对模型很自然,但对系统很难治理:路径在哪里、是否覆盖、内容是什么、风险多大,都混在一句话里。

更合理的 schema 应该把最小可执行意图拆出来:

{ "type": "object", "required": ["path", "content"], "properties": { "path": {"type": "string"}, "content": {"type": "string"}, "mode": {"type": "string", "enum": ["create", "overwrite", "append"]} } }

这样系统才能分别对路径应用path_policy,对mode做枚举检查,对高风险覆盖动作触发审批。

echo-agent 在Tool.to_schema()中会先验证参数 schema。数组 schema 必须有items,对象、anyOfoneOfallOf等结构也会递归检查。ToolRegistry.get_definitions()转换工具定义时,如果某个工具 schema 不合法,会跳过该工具并记录错误。

这一步很硬,但必要。非法 schema 如果进入 provider 层,可能导致模型请求失败;即使 provider 接受了,也可能让模型生成系统无法解析的参数。

参数校验发生在执行前。Tool.validate_params()会检查required字段、基础类型和枚举值。校验失败不会让工具内部抛出不可控异常,而是返回失败的ToolResult

errors = tool.validate_params(params) if errors: return ToolResult( success=False, error=f"Invalid parameters: {'; '.join(errors)}", )

这点很重要。参数错误也是一种观察,应该进入模型上下文。模型下一轮看到“缺少 path”或“mode 不在允许枚举内”,就有机会修正调用,而不是让整个 Agent Loop 崩掉。

执行现场

一次工具执行不是孤立动作。它属于某次请求、某个会话、某个用户和某条 trace。

ToolExecutionContext负责把这些现场信息带进工具层:

@dataclass(frozen=True) class ToolExecutionContext: execution_id: str = "" trace_id: str = "" session_key: str = "" user_id: str = "" agent_id: str = "" attempt_index: int = 0 idempotency_key: str = "" is_replay: bool = False parent_execution_id: str | None = None credentials: dict[str, str] = field(default_factory=dict) approved_actions: frozenset[str] = field(default_factory=frozenset) allowed_tools: frozenset[str] = field(default_factory=frozenset)

这里的字段不是摆设。execution_idtrace_id用于可观测性;session_keyuser_id用于权限、审计和隔离;credentials让工具按需读取凭证,而不是到全局环境乱取密钥;approved_actions承接审批结果;allowed_tools限制委派 worker 能调用哪些工具。

最容易被忽略的是idempotency_key

副作用工具不能被无意重复执行。读文件失败了可以重试,发送消息、写文件、创建任务、执行命令重复一次,就可能产生真实影响。

echo-agent 用trace_id、工具名、工具序号和排序后的参数生成幂等键:

def build_idempotency_key(trace_id, tool_name, index, params) -> str: payload = json.dumps( params, ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str, ) digest = hashlib.sha256( f"{trace_id}:{tool_name}:{index}:{payload}".encode() ).hexdigest() return digest[:24]

ToolRegistry.execute()在副作用工具执行前检查 replay cache。若发现同一执行范围内的重复副作用,会返回失败结果并阻止执行;执行成功后,再把幂等键写入缓存。缓存超过上限后按 LRU 淘汰。

这不能替代业务系统自己的幂等设计,但能防住同一 Agent 推理过程里的重复副作用。

只读工具主要改变模型认知;副作用工具会改变外部世界。工具系统的分水岭,正是副作用是否已经发生。

注册与暴露

工具多了以后,问题不再是“有没有这个工具”,而是“本轮模型应该看到哪些工具”。

echo-agent 通过discover_tools()按配置、workspace、消息总线、provider 和可选子系统组装工具。基础文件工具会注册,如ReadFileToolWriteFileToolEditFileToolListDirTool;搜索、补丁、待办、消息、澄清和通知工具也会加入。

执行类工具依赖配置。只有config.tools.exec.enabled打开时,才会创建执行器并注册 Shell 工具;代码执行和进程工具也依赖执行配置。网络工具还要看 web 配置和network_policy

如果系统提供了 task manager、workflow engine、session manager、scheduler、skill store、memory store 或 knowledge index,工具发现阶段会注册对应能力。图像生成和 TTS 工具也会根据配置尝试注册。

这说明工具发现不是固定目录扫描,而是运行时能力组装。

发现之后,还要经过filter_tools_by_policy()。配置入口包括:

class ToolsConfig(_Base): profile: Literal["minimal", "messaging", "coding", "full"] = "coding" allow: list[str] = Field(default_factory=list) also_allow: list[str] = Field(default_factory=list) deny: list[str] = Field(default_factory=list)

这层策略决定哪些工具可以暴露给模型。它会结合 profile、allow、also_allow、deny、安全 profile、capabilities 和网络策略做过滤。

但暴露策略不是最终安全边界。即使工具被模型看到,执行前仍会经过ApprovalGate、路径策略、执行器策略和工具内部校验。生产系统应该采用多层防御:少暴露、严审批、控执行、留审计。

层次作用
工具发现系统当前具备哪些能力
readiness这些能力当前是否可用
暴露策略本轮哪些工具给模型看
执行上下文本次调用属于谁、能做什么
审批与策略本次行动是否允许
执行日志发生过什么、能否复盘

执行治理

InferenceStage真正执行工具调用时,最终进入ToolRegistry.execute()

它不是一个普通字典查找,而是工具执行内核。流程可以概括为:

  1. 解析工具别名,如bash映射到exec

  2. 检查allowed_tools,防止受限 worker 越权。

  3. 查找工具对象。

  4. 校验参数。

  5. 构造或使用ToolExecutionContext

  6. 检查副作用工具 replay cache。

  7. 记录脱敏执行日志。

  8. 用超时和重试策略执行工具。

  9. 成功后记录 replay cache。

  10. 返回ToolResult

执行时用asyncio.wait_for控制超时:

result = await asyncio.wait_for( tool.execute(params, exec_ctx), timeout=tool.timeout_seconds, )

失败会按max_retries重试。最终失败时,系统返回ToolResult(success=False, error=...),而不是把未处理异常继续向上抛。

执行日志保存在_execution_log中,会记录工具名、脱敏参数、execution_idtrace_id、开始时间、成功状态和尝试次数。参数键名如果包含keytokensecretpasswordapi_keycredentialauth等敏感词,日志里会显示为"***"

这类日志对调试 Agent 行动很关键。模型说“我已经执行了某工具”不够,系统必须能查到:它何时执行、参数是什么、结果是否成功、重试了几次、有没有被策略拒绝。

结果反馈

工具输出不是最终回答,而是下一轮推理的观察。

echo-agent 用统一的ToolResult表达成功和失败:

@dataclass class ToolResult: success: bool = True output: str = "" error: str = "" metadata: dict[str, Any] = field(default_factory=dict) @property def text(self) -> str: return self.output if self.success else f"Error: {self.error}"

InferenceStage不需要理解每个工具的内部异常,只要把result.text写入 tool 消息。metadata则可以携带审批 ID、执行路径、结果数量、风险标记、输出文件位置等结构化信息,供审计和后续处理使用。

好的工具结果应服务下一轮决策。它至少应该说明是否成功、关键结果、错误原因、可采取的下一步、必要元数据和引用路径。

如果工具失败只返回“失败了”,模型无法恢复;如果返回几十万字符日志,模型会被噪声淹没。完整记录可以进入日志或文件,进入模型上下文的结果应该是可行动摘要。

对于副作用工具,结果还应说明影响范围:写入了哪个文件、执行器是什么、返回码是什么、是否被截断、是否触发审批、是否产生 artifact。

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

生产可用性

判断一个工具系统是否接近生产可用,不能只看“内置了多少工具”。工具数量越多,越需要治理。

更可检验的标准是:

检查项可检验标准
工具声明每个工具有清晰名称、描述、schema、风险等级和能力标签
Schema 质量非法 schema 在暴露前被跳过并记录错误
参数校验required、类型和 enum 错误能变成可恢复的工具失败
Readiness外部依赖缺失时启动阶段能报告不可用原因
分层暴露按 profile、allow、deny、capabilities 和网络策略过滤工具
权限审批副作用、高风险和执行类工具进入审批与策略判断
幂等保护写入、命令、消息、任务等副作用工具有 replay 防护
超时重试工具有 timeout 和有限重试,失败后返回结构化错误
审计日志记录工具名、脱敏参数、trace、结果和尝试次数
结果治理工具输出可行动、可引用、可截断,完整结果另行保存
委派收缩多 Agent worker 使用allowed_tools控制最小能力范围
回归评估有工具调用 trace、权限拒绝、重复副作用和参数错误样例

这里的核心判断很简单:工具系统不是给模型装上一堆外接能力,而是定义模型可以怎样接触世界。

如果工具过粗,模型会把大量隐含意图塞进自由文本,系统难以校验和审计;如果工具过细,模型需要承担过多编排工作,容易漏步骤、顺序错误或陷入循环。合适的粒度应该围绕语义动作:读文件、写文件、搜索知识、执行命令、创建任务、委派 worker、发送通知。

工具命名也不是美学问题。search_filesknowledge_search应该从名字上就能区分;execprocess应该分别表达一次性命令和后台进程。危险工具不应该用轻描淡写的名字。名称会影响模型选择、用户审批、日志审计和团队理解。

真正成熟的工具生态,不是把所有能力摊开给模型,而是让模型在正确场景看到正确能力,并让每次行动都能被解释、限制和复盘。

小结

Agent 能做事,不是因为模型“更聪明”,而是因为系统给了它一套受控的行动语言。

工具名和描述决定模型如何理解能力,schema 决定自然语言意图如何落成结构化参数,执行上下文决定本次调用属于谁、能做什么,幂等和审批限制副作用扩散,日志和结果反馈让行动可以进入下一轮推理和事后复盘。

工具系统的价值就在这里:它既让模型接触外部世界,又不把世界直接交给模型。

(全篇完)


本文为 echo-agent 设计笔记系列第 13 篇。项目源码已开源至 GitHub。如果你对工业级 Agent 的工程落地感兴趣,欢迎加入技术交流群参与日常讨论。下一篇我们将探讨 《Agent 执行器设计笔记:隔离命令、代码与进程》,敬请期待。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询