轻量级AI Agent框架设计:从工具编排到生产落地 📅 发布时间:2026/9/9 10:03:04 👁 浏览次数: 开头一段说一个实际场景——这周收到一份零散的需求要做一个“能听懂人话、自己去调工具、跑完还不需要人盯着”的小系统。说白了就是 agent。朋友催得急我连夜把 hermes-agent 推进到了一个能用的状态。这不是一个多复杂的东西但它从设计到落地花了我不少精力过程中该踩的坑一个没落下。今天就把整个项目的拆解、核心代码思路、实测翻车记录、还有后续演进方向一起写下来。想自己搭一个轻量 agent 的、或者正在犹豫用现成框架还是自研的可以拿这篇当个参考。1. hermes-agent 到底解决什么问题先聊聊我为什么要写这么个东西而不是直接搬 LangChain 或者 AutoGPT 这类现成方案。1.1 需求场景让一段话驱动多个工具完成跨环节任务需求来源其实很朴素我手上有一堆零散的内部小工具有查数据库的、有发请求的、有算指标的、有写日报的。每一个单独用都挺顺手但一旦要组合起来干活就得写一堆胶水代码。比如“拉一下上周的订单数据算个环比然后按模板生成一份分析摘要发到群里”——这种活儿在过去意味着几个脚本来回拼接、参数手工传递、中间出错还得重新跑。痛点在于工具的开发成本不高但工具的编排成本很高。更多人需要的不是又一个“调用大模型聊天的玩具”而是一个能用自然语言把已有能力串起来、按步骤执行、并且结果可控的系统。hermes-agent 这个名字也是从这个定位来的——它是数字世界的信使负责传达指令、分派任务、把结果带回来。1.2 为什么不自研一个“带提示词的工具调度器”这里有个常见的认知偏差很多人觉得 agent 就是“主提示词 几个 function call”写起来也就几百行不需要什么框架。这句话有道理但如果真做起来你会发现有几个问题不是几百行能解决的工具多了以后大模型怎么从二十个工具里准确选出那个组合工具调用结果是双层嵌套的 JSON模型下一轮到底该看哪一层内容调用失败后重试几次重试之前要不要先“消化”一下错误信息多步任务做到一半用户的原始意图还被记住吗如果用一个工具的输出当另一个工具的输入字段名对不上怎么办这些问题单独拎出来都谈不上复杂但全堆在一起就会变成“每个都处理一点点最后到处都是补丁”。我需要的是一个有清晰边界的分层结构让工具只负责“执行”让编排层只负责“决策”让记忆层只负责“上下文”。hermes-agent 就是按这个思路搭的核心是一个任务循环外围是工具注册表、上下文记忆和可插拔的模型适配层。1.3 项目想达到的三个核心指标在动手之前我给这个项目立了几个硬性指标后面所有设计都围绕它们展开可观测每一步决策、每一次工具调用、每一条上下文变化都要能打印出来而不是一个黑盒“啪”地给出结果。可替换模型层保持接口化换模型、换服务商不改动核心逻辑工具层用装饰器注册加工具不需要动主循环。可控制必须是“人类在环上”的设计——卡死、超时、连续调用异常时系统要主动停下来问人而不是自己无限循环。这三条看着简单但实际写代码的时候每一条都在逼自己重新审视设计。比如“可观测”这条直接决定了我要不要引入一个事件总线如果没有它日志就是一堆散乱的 print调试多步任务时会疯掉。2. 整体设计信使之神如何调度一堆“不会说话”的工具这一节走进架构层说清楚 hermes-agent 的各个模块是谁、干什么、和谁通信。这是整个项目最花心思的部分也是和你直接堆代码最大的区别所在。2.1 五层结构入口层、编排层、工具层、记忆层、通道层我先给出整体分层再逐个展开。每层职责单一不互相掺和层级职责关键模块类比入口层接收用户原话和附加元信息CLI、HTTP API、Webhook前台接待编排层维护任务循环决定“下一步做什么”TaskLoop、Planner、EventHandler项目经理工具层注册实际能力执行具体操作ToolRegistry、ToolExecutor、权限校验各职能部门记忆层管理短期上下文和长期事实ContextWindow、ConversationStore会议纪要员通道层连接外部模型服务和配置中心ModelAdapter、ConfigProvider后勤部门这个分层的核心理念是agent 的复杂不是靠一个聪明的函数来消灭的而是靠清晰的分工让每个环节都变成“模式化逻辑”。拿“项目经理”这个编排层举例它不需要知道工具是怎么实现的只需要在同一套抽象接口上调来调去同样工具层也完全不用关心当前是哪家大模型的哪个版本在“指挥”它。2.2 TaskLoopagent 最核心的“跑圈逻辑”TaskLoop 是整个 hermes-agent 的心脏。简单描述它的运行过程就是一个死循环加上四个状态判断。伪代码如下async def run(self, user_input): self.context.add(user, user_input) while True: # 1. 让模型基于当前上下文产生下一步动作 action await self.model_adapter.decide(self.context.snapshot()) # 2. 根据动作类型分流 if action.type final_answer: return action.content elif action.type tool_call: result await self.tool_executor.execute(action.tool_name, action.arguments) self.context.add(observation, result) self.event_bus.emit(tool_finished, action.tool_name, result) elif action.type ask_user: return await self.human_in_the_loop.confirm(action.question) else: self.event_bus.emit(invalid_action, action) self.context.add(error, 模型返回了未知动作类型请重新规划) self.counter.record_invalid() # 3. 控制最大轮数防止失控 if self.context.round() self.max_round: raise AgentLoopLimitError(f超过最大执行轮数 {self.max_round}任务终止)这套循环本身一点都不稀奇最关键的可能是action.type这个枚举。hermes-agent 把它限制为四种final_answer、tool_call、ask_user、invalid。为什么不能有第五种因为一旦让模型自由定动作类型你的控制逻辑就会变成 if-else 泥潭。限制动作空间是保证可控制的关键手段。2.3 模型输出为什么必须是“决策对象”而不是裸 JSON有些实现喜欢让模型直接输出一串 JSON然后用json.loads去解析。这种方案能跑但会让失败概率翻倍。原因有三模型容易多出一个 json的代码块标记直接loads 会炸。字段名不稳定上一轮叫tool_name下一轮可能成了name。出错时没有任何结构化的兜底方案只能重新让模型生成一遍浪费 token 不说可能还是错的。所以 hermes-agent 在模型适配层做了一个强制性的输出解析器。无论底层模型是 OpenAI 兼容接口、Claude 还是本地模型最终都要过一层DecisionParser把各种乱七八糟的原始输出解析成统一的AgentDecision对象。解析失败时不会直接抛异常终止循环而是构造一个invalid类型的决策对象告诉 TaskLoop“模型刚才说的话我没听懂你让它重新组织一下语言”同时把错误原因追加到上下文里。这个小小的兜底能把多步任务的成功率提升一大截。class AgentDecision(BaseModel): type: Literal[final_answer, tool_call, ask_user, invalid] tool_name: str | None None arguments: dict[str, Any] {} content: str | None None reason: str | # 模型解释自己为什么这么决策reason字段是我自认为很妙的设计。它不求质量多高但能让失败链路多一层“意图东西”。调工具出错时你可以把工具报错信息连同模型的原始 reasoning 一并打出来排错效率提升特别明显。2.4 通道层与事件总线调试多步任务不再靠猜上面提了几次event_bus这是 hermes-agent 在可观测性上最值得说的一笔。我实现了一个非常轻量的事件发布订阅机制不是消息队列别想复杂class EventBus: def __init__(self): self._subscribers: dict[str, list[Callable]] defaultdict(list) def subscribe(self, event_type: str, handler: Callable): self._subscribers[event_type].append(handler) def emit(self, event_type: str, **payload): for handler in self._subscribers.get(event_type, []): handler(Event(event_type, payload))事件类型目前有这些常量AGENT_STARTED/AGENT_FINISHED任务起止PLANNER_DECISION模型每轮决策内容TOOL_STARTED/TOOL_FINISHED/TOOL_ERROR工具调用的生命周期CONTEXT_UPDATED关键上下文变更HUMAN_CONFIRM_REQUIRED需要人工介入有了它你可以写一个RichConsoleReporter把决策链路用不同颜色打印出来也可以接一个logstash_handler把所有事件灌进 ELK。二次开发的时候你甚至可以订阅TOOL_ERROR事件自动给异常工具挂祭——比如连续调用两次失败之后系统自动在该工具名称后追加“该工具可能不可用请考虑替代方案”的系统提示。这个能力是我后面才加的但对复杂任务的稳定性帮助非常大。3. 核心代码骨架从任务解析到执行落地架构画完了接下来看具体代码实现。这一节我会挑几个核心模块展开给你能直接抄走的结构。3.1 工具注册表装饰器 反射 校验三件套工具层是所有能力的集合。hermes-agent 用register_tool装饰器把函数变成可被大模型调用的“标准件”。设计上参考了 FastAPI 依赖注入的思路但做得很轻TOOL_REGISTRY: dict[str, dict] {} def register_tool(name: str, description: str, parameters: dict): def decorator(func): wraps(func) async def wrapper(ctx: ToolContext, **kwargs): return await func(ctx, **kwargs) wrapper.metadata { name: name, description: description, parameters: parameters, } TOOL_REGISTRY[name] wrapper return wrapper return decoratorparameters必须是 JSON Schema 格式。为什么因为 OpenAI 的 function calling 和 Claude 的 tool use 都接受这个格式统一之后换模型提供商时工具定义一行不用改。下面是一个注册工具的真实例子register_tool( namequery_sales_data, description按日期范围查询销售订单明细返回金额、数量、类目等字段, parameters{ type: object, properties: { start_date: {type: string, description: 开始日期格式 YYYY-MM-DD}, end_date: {type: string, description: 结束日期格式 YYYY-MM-DD}, category: {type: string, description: 商品类目可选, enum: [A, B, C]}, }, required: [start_date, end_date], }, ) async def query_sales_data(ctx: ToolContext, start_date: str, end_date: str, category: str | None None): # 实际查数逻辑... return {rows: [...], total_amount: 12345.6, currency: CNY}务必注意一点JSON Schema 里的required字段不能写得太严。模型不是程序它对参数的填充经常不完整。对于一个非必填参数你宁可让它缺着也不能在 Schema 里强标 required。否则模型会猜一个错误的值填进去你拿到手还得再做一遍数据清洗纯属给自己找活。3.2 ToolContext工具和编排层之间的“会话暗号”工具不是孤岛它偶尔要读取任务级信息比如“当前用户在哪个时区”“这个任务是从哪个渠道进来的”“本次请求的 trace_id 是什么”。hermes-agent 为此定义了一个ToolContext对象每个工具被调用时都会自动注入dataclass class ToolContext: user_id: str conversation_id: str trace_id: str created_at: datetime extra: dict field(default_factorydict)为什么要单独设计这个对象而不是直接给工具传一个全局 session因为 agent 的一个重要特性就是“多任务并发”。事件循环里可能有三个任务同时在跑如果工具用的是全局 session欢快地互相污染数据那排查问题的时候你会想摔键盘。ToolContext把每个工具调用变成一次无共享会话的独立请求这是最保险的。另外一个实际作用工具日志里带着trace_id哪个任务里出了问题grep 一下日志就能串起整条链路。3.3 上下文管理不是所有信息都值得塞给模型上下文管理是 agent 系统里最容易被低估的模块。新手写 agent 最容易犯的错就是把所有观察结果、历史对话、工具响应一股脑地塞给大模型。某知名框架早期版本的默认行为真的会把几百 K 的文本堆进窗口然后让用户为 token 账单叹气。hermes-agent 的 ContextWindow 做了三件事按轮次保留消息超过max_context_messages默认 20 条就丢弃最旧的。给每条消息加importance分数工具执行失败、包含用户核心指令的条目优先级极高不会被轻易挤掉。对长输出做摘要预处理。每次工具返回结果先过一个summarizer小型模型或基于 key sentence 的抽取器把 5000 字的原始结果压缩成 200 字的执行摘要再把摘要放进上下文窗口。这段代码是摘要预处理的示例class ContextCompressor: def __init__(self, max_observ_chars: int 600): self.max_observ_chars max_observ_chars async def compress(self, observation: str) - str: if len(observation) self.max_observ_chars: return observation # 简单抽取式摘要优先保留数字、表头、结论句 lines observation.split(\n) kept [line for line in lines if self._is_important_line(line)] truncated \n.join(kept[:30]) return truncated[: self.max_observ_chars] ...(已截断详见完整记录) def _is_important_line(self, line: str) - bool: # 包含数字、金额、百分比、总结词的句子优先保留 return any(mark in line for mark in [总, 合计, %, 元, 结论, Error, 错误])这个模块上线前我做过一轮极端测试让工具返回一个 100KB 的服务端日志看模型还能不能准确回答“日志中出现了几次 ERROR”。没有摘要处理器时模型答非所问加了之后准确率回到 95% 以上。所以永远不要迷信“大模型上下文窗口很大塞就完了”窗口大不等于注意力好。3.4 模型适配层如何避免被一家模型厂商绑架很多 agent 项目刚开始只适配 OpenAI 兼容接口写得很高兴。等你想切换到 Claude 或本地 Qwen 的时候发现代码里到处是openai.ChatCompletion改起来想死。hermes-agent 从一开始就定了ModelAdapter抽象class BaseModelAdapter(ABC): abstractmethod async def decide(self, messages: list[dict], tools: list[dict]) - AgentDecision: pass abstractmethod async def generate(self, prompt: str, **kwargs) - str: pass然后分别实现了OpenAICompatAdapter、ClaudeAdapter、OllamaAdapter。用过哪种多少钱、哪种延迟低这些我不评价毕竟环境不同。但接口统一这件事本身带来的好处不是“换模型方便”五个字能概括的——团队里如果有几个人在测不同模型你只需要改配置文件里的model_provider字段就能对比评估而不是每次切换都要改代码。配置示例model: provider: openai_compat base_url: https://api.example.com/v1 api_key_env: LLM_API_KEY model_name: hermes-pro temperature: 0.2 max_tokens: 4096顺手记录一个关键经验agent 场景的 temperature 一定要调低甚至直接设为 0 或 0.1。你不需要模型有创意你需要它稳定地输出有效 JSON。一个发散性极强、每次都给你变着花样写 reasoning 的模型会让你崩溃到怀疑人生。4. 从零跑通一个完整 Demo跨工具编排是怎么工作的光讲架构太虚这里用一个具体的任务来完整演示 hermes-agent 的干活流程。4.1 案例任务查询订单 → 计算环比 → 写日报 → 输出摘要这个任务是一个典型的多步骤编排场景。让模型看不到中间细节直接看效果。我提前注册了三个工具query_sales_data查询订单表返回总量和金额。calc_growth_rate计算本期与上一期的环比增速。save_daily_report按模板写报告文案附加到指定 Markdown 文件。任务原话是“查一下这周和上周的销售数据算算环比然后写进日报文件里最后告诉我结论。”在 CLI 中执行python -m hermes_agent cli --task ...整个过程会输出类似下面这样的事件流[AGENT_STARTED] 收到任务查一下这周和上周的销售数据... [PLANNER_DECISION] 类型tool_call 工具query_sales_data 参数{start_date:2025-05-12,end_date:2025-05-18,category:null} 理由先获取本周销售数据 [TOOL_STARTED] query_sales_data [TOOL_FINISHED] 返回 4 行结果total_amount284730.00 [PLANNER_DECISION] 类型tool_call 工具query_sales_data 参数{start_date:2025-05-05,end_date:2025-05-11,category:null} 理由再获取上周销售数据作为对比 [TOOL_FINISHED] 返回 4 行结果total_amount256890.00 [PLANNER_DECISION] 类型tool_call 工具calc_growth_rate 参数{current:284730.0,previous:256890.0} 理由计算本周与上周的环比增长率 [TOOL_FINISHED] 返回 {growth_rate: 10.84} [PLANNER_DECISION] 类型tool_call 工具save_daily_report 参数{content:本周销售额 ... 环比增长 10.84%} [TOOL_FINISHED] 已写入 reports/2025-05-19.md [PLANNER_DECISION] 类型final_answer 内容本周销售额 284730 元较上周增长 10.84%日报已更新。 [AGENT_FINISHED] 任务完成看这个流程你会发现模型并不是“预设好步骤再执行”而是每走一步都根据上一步的实际结果重新规划下一步。这就是 agent 比普通工作流脚本聪明的地方如果第一次查询返回 0 行模型可能会据此修正查询条件而不是继续傻乎乎地算环比。4.2 模型到了第 5 轮突然乱答加一层“任务再规划”机制上面那么美好的流程不代表每次都会顺利。实测中我遇过最恼火的情况是前三步还很正常到第五步模型像突发恶疾似的开始要求调用一个不存在的工具名或者甩出大段废话而没有任何结构化输出。以前我是删掉上下文重启后来逐渐摸清这类问题大多是两种原因造成的上下文窗口太拥挤模型注意力被早期工具返回的海量数据带偏。对话轮次太长模型把“边角料信息”误认为“核心命令”。针对第一种我优化了ContextWindow的截断策略把工具返回数据强制压到“摘要模式”。针对第二种我设计了Replanner——当执行到第 N 轮时用一个轻量的提示词让模型总结一下“当前任务的最终目标是什么、已完成哪些步骤、剩余哪些步骤”把这个总结放回系统消息的最前面。这个“任务再规划”的效果极好。尤其是在长任务超过 8 轮中它的成功率提升可以用“质变”来形容——背后的逻辑也很简单每轮都在喂模型一次“我是谁、我在哪、我要去哪”相当于给一个健忘症患者不断重复路线图。4.3 本地跑通 Demo 的环境准备清单整个项目依赖很少Python 3.11 起步核心依赖就四个pydantic数据校验、httpx异步请求、pyyaml配置、rich日志美化。LLM 调用走 OpenAI 兼容接口所以只要 chat/completions 接口兼容什么服务商都能接。git clone https://github.com/your/hermes-agent.git cd hermes-agent python -m venv .venv source .venv/bin/activate pip install -e .[dev] cp .env.example .env # 编辑 .env填入 LLM_API_KEY 和模型配置 hermes-agent cli --task 你好介绍一下你自己注意.env.example里预留了这些字段LLM_API_KEYsk-xxxx LLM_BASE_URLhttps://api.example.com/v1 LLM_MODEL_NAMEhermes-pro LLM_TEMPERATURE0.1 AGENT_MAX_ROUND15 AGENT_DEFAULT_VERBOSEfalseAGENT_MAX_ROUND一定要设置不设置的话一旦模型陷入死循环你的任务就会无限调用工具等你想起来还有 token 费用这件事的时候账单已经很感人了。5. 实测中踩过的坑和边界问题这一节是我最想让读者看到的地方。agent 开发里很多问题只有跑起来才知道README 和教程不会写。5.1 工具返回双层列表模型解析后字段成了人生哲学我第一次跑通时工具返回的结构是{data: [{city: 北京, value: 100}, {city: 上海, value: 200}]}。模型第三次调用时把参数写成了city: 北京上海直接在value字段里填了200 100。从人的角度看很蠢但站在模型的角度它只是想把“两个城市的数值”拼成一个参数传回去。这里本质问题是JSON Schema 中没有限定“数组内元素的卡片结构”模型无法理解data是对象数组而把外层结构直接压平了。解决办法没有银弹只有三板斧Schema 里把items的描述写得更细、明确提示“该参数对应数组内元素的某个字段不是整个数组”、在工具入口加上一个轻量的合法性解析器参数不合理时直接抛出带人话的错误信息让模型自己去“看”。这三板斧叠加后这类错误发生率能降到 5% 以下。下面的代码展示了如何在 Schema 描述里写“人话”parameters{ type: object, properties: { city: { type: string, description: 目标城市名必须在返回结果 data 数组中某个元素的 city 字段里选择不要拼接多个城市, examples: [北京], }, value: { type: number, description: 该城市对应的 value 数值必须是具体数字不能是空格分隔的多个数字, }, }, }5.2 循环依赖agent 自己把自己卡死第二次踩到的坑是 Agent 在执行过程中会“反思”“我刚才的工具调用也许可以换个参数再试一次。”于是同一个工具用几乎相同的参数试了 8 次每轮都返回同样的错误每轮都同样乐观地重试。这不是模型笨而是因为我在系统提示词里写了“你可以多次尝试使用工具”模型把这句话理解成了“死磕到底”。修复方式是引入RetryPolicy对同一工具相同参数签名最多重试两次。第三次如果还是相同错误直接把该工具标记为“当前不可用”重新规划或者直接让模型向用户坦白“我尝试了三次但都不成功”。这个策略还能防止很多 agent 系统都有的“抖机灵”式修复——明明数据查出来是错的模型不去验证而是编一个合理借口这种隐蔽幻觉只有通过强制重试上限来压制。dataclass class ToolRetryPolicy: max_retry_same_signature: int 2 disable_after_exceed: bool True async def execute_with_retry(tool_name: str, arguments: dict, policy: ToolRetryPolicy): errors [] for attempt in range(policy.max_retry_same_signature 1): result await execute_tool(tool_name, arguments) if not result.is_error: return result errors.append(result.error_message) # 相同错误信息直接标记不可用 if len(set(errors)) 1 and attempt policy.max_retry_same_signature: mark_tool_unavailable(tool_name) raise ToolUnavailableError(f工具 {tool_name} 连续 {policy.max_retry_same_signature 1} 次相同错误已暂停使用) return result5.3 长任务上下文爆窗截断策略与记忆压缩的平衡第三个坑可以说是所有 agent 的宿命任务还远远没跑完上下文窗口已经快满了。尤其是工具返回结果是纯文本报表、代码片段或一大段 JSON 的时候窗口消耗的速度比想象中快得多。我的方案分三层第一层是优先截断最旧的工具观察结果保留系统提示词、用户原始指令和最近两轮的决策记录。第二层是对工具输出做“模式摘要”如果是报表只保留表头、总行数、总金额和异常标记如果是代码只保留文件名、函数列表和错误栈第一行如果是 JSON保留 key 路径和值类型。第三层是长任务“归档”ConversationStore会自动把前 N 轮做成一个压缩包存进long_term区需要时通过一个recall_tool取回。这三层下来跑一个 20 轮以内的复杂任务上下文窗口占用基本能稳定在 40% 以下。我认为这是 agent 工程化中收益最大的一项投资。5.4 多任务并发的资源隔离最后一个坑是关于并发的。一开始我觉得 Python 单进程就行反正任务也不多。后来实际使用时才发现我会同时给 agent 发三四个任务互相之间如果要共用同一个工具实例状态就会被不同任务的中间结果污染。比如任务 A 更新了一个公共内存缓存任务 B 读到的就是 A 的脏数据任务 A 调用save_daily_report写入文件时任务 B 也在写同一份文件——两个任务直接撞车。解决方式不复杂给每一个任务分配一个独立的TaskContext和一个独立的临时工作目录工具操作都限定在该目录内涉及共享文件的工具加一个文件锁。同时所有工具的副作用操作尽量“追加”而不是“重写”至少保证并发时不会互相覆盖。6. 往生产环境演进时我的一些思考从本地 demo 到真正可用的服务中间还隔着一层不小的距离。这里把我目前的思考记录一下也算给未来动手的人划个重点。6.1 从 CLI 到 HTTP API加一层薄薄的 gatewayCLI 适合本地测试但一旦接入内部群机器人、网页控制台或者定时任务就需要一个 HTTP 服务层。hermes-agent 里我实现了一个轻量 FastAPI 封装只暴露三个接口POST /agent/run同步执行完整任务、POST /agent/run-async异步提交任务返回 task_id、GET /agent/tasks/{task_id}查询任务状态与事件流。这里有件事必须提醒agent 任务不是普通 HTTP 请求那样几百毫秒就能返回的。一个真实的多步骤任务可能要跑 30 秒甚至几分钟。如果网关层按常规做法设置了超时 5 秒任务基本全灭。我一开始就踩过这个坑后来在异步模式下引入类似“事件流订阅”的机制客户端提交任务后通过 SSE 接收 AgentEvent实时看到每一步的进展。体验和调试效率都好了非常多。6.2 可观测性给 every 事件挂上 trace_id 和 cost 统计agent 的可观测性比普通服务更重要因为它不是一个确定性的计算结果而是一棵“决策树”。所以我做了两件事给每个 TaskContext 发一个全局唯一的 trace_id所有日志、事件、工具输出都带上。这样即使在多任务并发时也能一键捋清“哪个任务出的问题”。统计每个模型请求的 token 消耗、每轮调用的工具耗时。最终在任务结束时输出一个TaskReport包含总轮数、总耗时、token 使用量、各工具调用次数、总体成功状态。下面是我的日志输出风格示例[2025-05-19 14:02:11] [trace3f9d2] [round2] [model] prompt_tokens3409 completion_tokens312 [2025-05-19 14:02:11] [trace3f9d2] [round2] [tool] query_sales_data 执行耗时 182ms [2025-05-19 14:02:11] [trace3f9d2] [round2] [event] TOOL_FINISHED query_sales_data这个体验其实特别接近以前搞 Linux 管道或大数据任务的排查方式——不追求找到一个魔法日志只追求每个环节都有记录且记录之间能串起来。6.3 权限与安全agent 能调用工具但工具不能越权这是生产化进程中我最坚持的一点Agent 可以智能地使用工具但工具本身必须遵守权限边界。本地 demo 时可以允许save_daily_report随便写文件但一旦部署成服务就必须在 ToolContext 里带上role/permissions字段所有工具执行前过一道权限检查。比如普通用户只能查询数据不能写库、不能删文件。管理员可以执行写操作但所有写操作必须记录审计日志。涉及金额、用户隐私的工具默认要求人工二次确认。这个权限层本质上和数据面网关很像。Agent 是大脑权限层是肌肉的“肌腱限制”——大脑想抬腿踢人但肌腱的物理限制让它只能抬到一定高度。代码往里加可能简单但意识上如果不从一开始就建立后续迟早要重构补课。6.4 下一步规划多 Agent 协作与工具自描述目前 hermes-agent 还是一个“单 Agent 多工具”的架构。能不能前进到“多 Agent 协作”比如一个 Planner Agent 负责任务拆解一个 Researcher Agent 负责查资料一个 Reviewer Agent 负责检查结果。这个方向我在评估最重要的判断标准是多 Agent 带来的调度复杂性和 token 成本是否真的能让任务质量有显著提升。目前看对于常规的内部业务流程单 Agent 加上工具编排就够了但如果你有那种包含大量主观判断、需要来回验证的任务多 Agent 的“对抗式”结构确实有价值。工具自描述是我的另一个重点方向。与其在 JSON Schema 里手写描述不如让工具定义具备“自省能力”从函数的 docstring、类型注解和实际返回值中自动生成描述。这个想法实现起来有难度但一旦做成工具接入成本能进一步降低agent 的扩展性也会上一个台阶。最后说一点个人感受。做 hermes-agent 的过程我最大的体会是agent 领域其实还没有一个“银弹框架”无论你选哪套开源项目做底座最后都会走到“自己改造”这条路。所以别太纠结框架对比先把你的业务场景梳理清楚想明白哪些步骤必须可控、哪些环节必须可观测然后用一套灵活的最小核心去接你的真实工具。把编排、工具、上下文、事件这四件事拆干净你的 agent 就已经赢过了很大一部分“什么都往提示词里塞”的玩具项目。