
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, toolstool_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 的实现为例。它的工具系统由Tool、ToolExecutionContext、ToolResult、ToolRegistry、discover_tools、filter_tools_by_policy和ApprovalGate等模块共同组成。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对象、anyOf、oneOf、allOf等结构也会递归检查。ToolRegistry.get_definitions()转换工具定义时如果某个工具 schema 不合法会跳过该工具并记录错误。这一步很硬但必要。非法 schema 如果进入 provider 层可能导致模型请求失败即使 provider 接受了也可能让模型生成系统无法解析的参数。参数校验发生在执行前。Tool.validate_params()会检查required字段、基础类型和枚举值。校验失败不会让工具内部抛出不可控异常而是返回失败的ToolResulterrors tool.validate_params(params) if errors: return ToolResult( successFalse, errorfInvalid parameters: {; .join(errors)}, )这点很重要。参数错误也是一种观察应该进入模型上下文。模型下一轮看到“缺少 path”或“mode 不在允许枚举内”就有机会修正调用而不是让整个 Agent Loop 崩掉。执行现场一次工具执行不是孤立动作。它属于某次请求、某个会话、某个用户和某条 trace。ToolExecutionContext负责把这些现场信息带进工具层dataclass(frozenTrue) 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_factorydict) approved_actions: frozenset[str] field(default_factoryfrozenset) allowed_tools: frozenset[str] field(default_factoryfrozenset)这里的字段不是摆设。execution_id和trace_id用于可观测性session_key和user_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_asciiFalse, sort_keysTrue, separators(,, :), defaultstr, ) 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 和可选子系统组装工具。基础文件工具会注册如ReadFileTool、WriteFileTool、EditFileTool、ListDirTool搜索、补丁、待办、消息、澄清和通知工具也会加入。执行类工具依赖配置。只有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_factorylist) also_allow: list[str] Field(default_factorylist) deny: list[str] Field(default_factorylist)这层策略决定哪些工具可以暴露给模型。它会结合 profile、allow、also_allow、deny、安全 profile、capabilities 和网络策略做过滤。但暴露策略不是最终安全边界。即使工具被模型看到执行前仍会经过ApprovalGate、路径策略、执行器策略和工具内部校验。生产系统应该采用多层防御少暴露、严审批、控执行、留审计。层次作用工具发现系统当前具备哪些能力readiness这些能力当前是否可用暴露策略本轮哪些工具给模型看执行上下文本次调用属于谁、能做什么审批与策略本次行动是否允许执行日志发生过什么、能否复盘执行治理当InferenceStage真正执行工具调用时最终进入ToolRegistry.execute()。它不是一个普通字典查找而是工具执行内核。流程可以概括为解析工具别名如bash映射到exec。检查allowed_tools防止受限 worker 越权。查找工具对象。校验参数。构造或使用ToolExecutionContext。检查副作用工具 replay cache。记录脱敏执行日志。用超时和重试策略执行工具。成功后记录 replay cache。返回ToolResult。执行时用asyncio.wait_for控制超时result await asyncio.wait_for( tool.execute(params, exec_ctx), timeouttool.timeout_seconds, )失败会按max_retries重试。最终失败时系统返回ToolResult(successFalse, error...)而不是把未处理异常继续向上抛。执行日志保存在_execution_log中会记录工具名、脱敏参数、execution_id、trace_id、开始时间、成功状态和尝试次数。参数键名如果包含key、token、secret、password、api_key、credential、auth等敏感词日志里会显示为***。这类日志对调试 Agent 行动很关键。模型说“我已经执行了某工具”不够系统必须能查到它何时执行、参数是什么、结果是否成功、重试了几次、有没有被策略拒绝。结果反馈工具输出不是最终回答而是下一轮推理的观察。echo-agent 用统一的ToolResult表达成功和失败dataclass class ToolResult: success: bool True output: str error: str metadata: dict[str, Any] field(default_factorydict) property def text(self) - str: return self.output if self.success else fError: {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_files和knowledge_search应该从名字上就能区分exec和process应该分别表达一次性命令和后台进程。危险工具不应该用轻描淡写的名字。名称会影响模型选择、用户审批、日志审计和团队理解。真正成熟的工具生态不是把所有能力摊开给模型而是让模型在正确场景看到正确能力并让每次行动都能被解释、限制和复盘。小结Agent 能做事不是因为模型“更聪明”而是因为系统给了它一套受控的行动语言。工具名和描述决定模型如何理解能力schema 决定自然语言意图如何落成结构化参数执行上下文决定本次调用属于谁、能做什么幂等和审批限制副作用扩散日志和结果反馈让行动可以进入下一轮推理和事后复盘。工具系统的价值就在这里它既让模型接触外部世界又不把世界直接交给模型。全篇完本文为 echo-agent 设计笔记系列第 13 篇。项目源码已开源至 GitHub。如果你对工业级 Agent 的工程落地感兴趣欢迎加入技术交流群参与日常讨论。下一篇我们将探讨 《Agent 执行器设计笔记隔离命令、代码与进程》敬请期待。