1. hermes-agent 到底解决什么问题
1.1 名字背后的定位
hermes-agent 这个名字,懂点希腊神话的朋友一眼就能看出来源。赫尔墨斯是众神之间的信使,管的就是信息传递和跨界沟通。给一个智能体项目起这个名字,其实特别贴切:LLM 本身只会在对话框里生成文本,它不知道你公司内部有什么系统,也无法替你点按钮、查数据、发消息。而 hermes-agent 要扮演的,正是那个替你把指令从自然语言翻译成可执行动作、再把执行结果翻译回人能看懂的答案的"信使"。
我在刚接手这个项目时,团队正在被一堆重复性脏活折磨:每天要有人去不同后台手动查报表、汇总告警、跟进工单状态、把结果贴到群里。这些事逻辑不复杂,但频率高、操作碎,写一次性脚本又撑不了两个星期。于是我们决定做一个以自然语言为入口的任务型智能体:业务同学直接说"帮我把华东区昨天的订单量按渠道拉一张表,并同步给运营群",agent 自己去查数、渲染、推送。这个目标听起来简单,落地时涉及的东西比想象中多得多,也是这篇文章想完整讲清楚的部分。
1.2 和普通 LLM 应用的差别
很多人第一次接触 agent 概念时,会把它理解成"套了层壳的 ChatGPT"。实际上,普通 LLM 应用和 agent 之间的差距,可以从几个维度看得非常清楚。
| 维度 | 普通 LLM 应用 | hermes-agent 这类智能体 |
|---|---|---|
| 调用模式 | 一次请求一次生成 | 多轮"推理 - 行动 - 观察"闭环 |
| 输出形态 | 纯文本 | 可能包含工具调用、状态变更、外部系统动作 |
| 能力边界 | 受限于模型训练知识 | 模型知识 + 外部工具 + 实时数据 |
| 典型场景 | 问答、文案、翻译 | 自动查数、任务编排、跨系统操作 |
| 失败代价 | 低,重新生成一次就行 | 高,可能有副作用,需要校验与回滚 |
这个对比想说明的核心观点是:agent 的本质是把"思考"和"行动"接成了一个闭环。模型每次生成不再只是一段话,而可能是一份"行动计划",比如调用哪个工具、传入什么参数。系统执行完计划,把结果喂回给模型,模型根据新信息决定下一步。这个闭环让 agent 能处理需要多步操作才能完成的任务,但同时也引入了一个巨大的副作用:中间任何一环出错,结果都可能偏离预期,而且问题往往不是一次性出现,而是累积出来的。
1.3 适合谁、不适合谁
在做技术选型时,我习惯先把"不适合"讲清楚。如果只是想要一个能聊天的窗口、做内容生成、或者回答固定 FAQ,直接用普通 LLM API 就够了,引入 agent 这层复杂度纯属给自己找麻烦。agent 真正有价值的前提,是同时满足三个条件。
第一,你手里有稳定、语义清晰的工具或 API。agent 的战斗力全部来自它能调用的工具,如果平台本身接口都不稳定,agent 只是在放大不稳定。第二,任务存在明确的决策点,比如"先查库存,再决定是否下单"这类需要根据中间结果动态调整的流程。第三,你能接受一定的失败概率,并且有办法通过日志、审批、重试来兜底。
反过来,如果你的场景是纯即时交流、没有外部系统可调、或者某个操作一旦出错代价极高且不可逆,那我建议还是先用传统流程加人工确认,不要急着上 agent。这不是能力问题,而是风险控制问题。把 agent 用在对的地方,它价值极大;用在错的地方,它就是个会一本正经闯祸的实习生。
2. 架构设计与关键技术选型
2.1 整体架构:五个关键层
hermes-agent 的整体架构,我按职责拆成了五层。这个拆法不是一开始就有的,是踩了无数坑之后总结出来的,每一层负责的事情必须单一、清晰,否则出问题的时候根本定位不到是哪一环在撒谎。
- 会话入口层:负责接收用户消息、管理会话 ID、做基础鉴权和限流。它不关心业务逻辑,只负责把请求安全地送进流程。
- 规划调度层:这是 agent 的核心,也就是所谓的 agent 主循环。它决定"该不该调用工具、调用哪个工具、什么时候结束"。
- 工具注册层:把外部能力(查库、发消息、调用内部 API)统一声明成结构化 schema,供模型理解和调用。
- 记忆层:管理多轮对话上下文,包括原始消息、工具结果、历史摘要,控制上下文窗口不被撑爆。
- 执行与观测层:真正去执行工具调用,记录全链路日志、耗时、token 消耗,供排查和审计。
为什么一定要分层?我给新人的比喻是:做主循环就像在做一场直播,会话层是导播台,调度层是主持人,工具层是各种嘉宾,记忆层是提词器,观测层是录像系统。任何一层出问题,你都得知道该切哪路信号。实际开发里,90% 的难缠问题出在调度层和工具层的衔接处,如果不分层,你只会看到"结果不对"这四个字,然后无从下手。
2.2 工具注册机制:一切能力皆可声明
工具注册是整个 agent 的地基。模型不了解你的系统里有什么,全靠你通过 schema 告诉它。这里我选择了标准的 JSON Schema 来描述工具,原因很直接:它同时是"给模型看的说明书"和"给程序做的校验条"。
写工具 schema 有三个必须注意的细节。第一,name 要见名知义,比如 query_order_stats 就比 do_something 强一百倍,模型看到名字就能猜到大概用途。第二,description 要写清楚触发条件、参数含义、返回结构,甚至边界情况,模型对工具的误用,一半以上是描述写得含糊导致的。第三,parameters 要精确到类型、枚举值、默认值和必填项,能加约束就加约束。
我用一个简化示例展示注册逻辑。
from pydantic import BaseModel TOOL_REGISTRY = {} def register_tool(name, description, args_model): def decorator(func): TOOL_REGISTRY[name] = { "func": func, "description": description, "args_model": args_model, } return func return decorator class QueryOrderArgs(BaseModel): region: str = "华东" channel: str | None = None start_date: str end_date: str @register_tool( "query_order_stats", "查询指定区域、渠道、时间范围内的订单量统计,返回按天聚合的订单数和销售额", QueryOrderArgs, ) def query_order_stats(region: str, channel: str | None, start_date: str, end_date: str): # 这里对接数仓或订单服务的真实查询 return {"region": region, "days": 7, "order_count": 1024, "sales": 88468.5}用 Pydantic 定义参数模型有两个明确的好处。第一,它能自动生成标准 JSON Schema,供模型的 function calling 接口使用;第二,模型传回的参数在真正调用函数之前会先经过 model_validate 校验,格式不对当场拦住,而不是让错误一路穿透到业务代码里。
2.3 模型与协议选型的关键决策
工具注册层搭好后,下一步是选模型和定协议。我在这块做过三个比较关键的决定,每个背后都有实际踩坑的教训。
第一个决定是模型必须原生支持 function calling 或 tool use。这个能力意味着模型在生成回复时,可以输出结构化的"工具调用请求",系统再把它解析成具体动作。选型时优先看模型对 JSON 输出的稳定度,而不是只看它在通用榜单上的分数。有些模型聊天很强,一让它输出工具调用就频繁给错参数名,这种模型做 agent 会非常痛苦。
第二个决定是模型接入层统一走 OpenAI 兼容协议。原因很现实:市面上主流模型服务商基本都兼容这套协议,封装一层之后,切换模型只改配置不改代码。我自己在项目里维护了一个很薄的 client,把模型名、base_url、key 都做成配置项,这为后续换更便宜的模型、做灰度对比省了大量时间。
第三个决定是工具调用场景下把 temperature 调到 0.1 到 0.3 之间。温度太高,模型会"创造性"地编造参数;温度太低,某些模型在需要发散思考的任务上又会显得死板。我的经验是,工具选择和参数生成这类任务,低温度的正确率提升是肉眼可见的。创意文案之类的另起一个高温度实例,不要让一个实例同时承担两种矛盾的需求。
至于为什么不用现成的 agent 框架直接套,我当时综合评估过 LangChain 这类方案。结论不是它们不好,而是对我们这种团队来说,自己维护一个 200 行左右的主循环,可控性远大于引入一个上千依赖的框架。出问题时你能看懂每一行在干什么,改起来也快,这是框架很难给到的确定性。
3. 实操:从零搭出可用的 agent 核心循环
3.1 最小可运行骨架
理论讲完,直接上代码。一个最小可用的 agent 主循环,不需要花哨的设计,核心就是四件事:发消息、收响应、判断有没有工具调用、有就执行并把结果回传。下面是去掉业务细节后的骨架。
import json from openai import OpenAI client = OpenAI(base_url=MODEL_BASE_URL, api_key=MODEL_API_KEY) SYSTEM_PROMPT = """你是一个任务执行智能体。你可以调用工具完成用户请求。 每次只做一步,观察工具返回结果后再决定下一步。 如果任务完成或信息不足且没有可用工具,请用文字明确回答。""" def run_agent(question: str, max_steps: int = 8): messages = [{"role": "system", "content": SYSTEM_PROMPT}] messages.append({"role": "user", "content": question}) for step in range(max_steps): resp = client.chat.completions.create( model=MODEL_NAME, messages=messages, tools=build_tool_schemas(), temperature=0.2, ) msg = resp.choices[0].message messages.append(msg) if not msg.tool_calls: return msg.content or "任务完成" for tc in msg.tool_calls: name = tc.function.name args = json.loads(tc.function.arguments) meta = TOOL_REGISTRY[name] try: validated = meta["args_model"].model_validate(args) result = {"ok": True, "data": meta["func"](**validated.model_dump())} except Exception as exc: result = {"ok": False, "error": str(exc)} messages.append({ "role": "tool", "tool_call_id": tc.id, "content": json.dumps(result, ensure_ascii=False), }) return "已达到最大执行步数,任务可能未完成。"这段代码虽然短,已经包含了 agent 闭环里最关键的元素。注意我把工具执行结果统一包了一层 {"ok": ..., "data" / "error"} 再回传模型,这个包装是故意的:模型能清楚看到工具是成功还是失败,失败信息是什么,然后它会基于错误自行纠正。如果不做这个包装,工具抛异常直接导致整个 agent 崩掉,用户体验会非常糟糕。
3.2 工具参数校验:防幻觉的第一道防线
在上面的代码里,我特意在调用真实函数之前加了 validated = meta["args_model"].model_validate(args) 这一步。这不是多此一举,而是血泪教训换来的。
模型在生成 arguments 时,本质是在做概率预测,难免出现幻觉参数。常见情况包括:日期格式不对、枚举值写错、把必填参数漏掉、甚至给函数名加上了不存在的参数。如果这些数据直接打进业务函数,轻则查询报错,重则写库写错。用 Pydantic 校验之后,不合法的参数会在边界处被拦截,模型拿到 {"ok": false, "error": "..."} 的结果后,通常会自动调整参数再试一次,这在多数情况下能自愈。
我建议给这类校验再加一层业务约束,比如日期范围必须 start_date 早于 end_date,渠道必须在白名单内。Pydantic 的 model_validator 可以很方便地做跨字段校验。校验规则写得越细,模型越不容易胡来。本质上,工具 schema 既是给模型的约束,也是给系统的保护罩,两者缺一不可。
3.3 异常兜底与步数预算
Agent 循环最怕的就是没有收敛条件。如果没有 max_steps 限制,模型理论上可以无限调用工具,产生大量 token 费用不说,还可能反复执行同一个失败操作。我给它的默认值是 8,具体任务会根据复杂度调整,但一定有上限。
除了步数预算,还有两个从生产环境总结出来的兜底经验。
第一,给工具执行加超时。外部 API 慢、超时、假死都是常态,一个工具卡住,整个 agent 就卡住了。工具调用最好放在带超时的执行器里,比如用 asyncio.wait_for 包装,超时后返回明确的错误信息给模型,让模型决定换一种方式完成任务。
第二,所有有副作用的工具都要考虑幂等性。比如"下单""发消息""改配置"这类操作,一旦模型因为网络抖动重复调用,可能造成重复下单、重复发送。我的做法是给写操作工具增加 request_id 或操作确认参数,服务端在接收端做去重。agent 的容错不该只靠模型自觉,系统设计上必须为重复执行做好准备。
3.4 多轮记忆与上下文控制
多轮对话场景下,messages 数组会越长越多。每个工具的结果、每轮模型的推理,都在消耗上下文窗口。如果不做控制,用不了几轮就会触达 token 上限,后面的请求直接报错,或者费用高得离谱。
我的上下文管理策略分三级。第一级是截断:只保留 system 提示词、最近的 N 轮对话和当前待处理的工具结果,更早的内容全部丢弃。第二级是摘要:把被截断的旧对话定期压缩成一段摘要,作为 system 的一部分继续提供背景信息。第三级是针对超长工具结果的特殊处理:查询返回几千行明细时,不让模型接口直接接收全部明细,而是先做聚合或只回传前几十条,需要详查时再让模型调用专门的翻页工具。
记忆这块最容易犯的错是贪多。总想把所有历史都喂给模型,结果上下文里塞满了噪声,模型反而抓不住重点,回答质量下降。实际经验是,tool 结果只保留与当前任务相关的部分,历史摘要控制在三四句话以内,效果通常比一股脑全塞要好得多。
4. 常见问题与排查实录
4.1 高频翻车现场
场景一:模型传了根本不存在的参数
这是上线初期出现频率最高的问题。模型明明看到了工具 schema,却在调用时给参数名稍作变形,比如把 start_date 写成 startDate,或者把 channel 写成 channel_name。这类问题在换模型后尤其明显,不同模型的 function calling 遵循度差别很大。
排查时的第一反应不要怪模型,先检查自己的 schema 写得好不好。参数名是否是常见命名习惯,description 是否说清楚了取值范围。如果 schema 没问题,再考虑给参数校验层加上"近似匹配"或"错误提示",让模型在一两次失败后能自我纠正。
场景二:agent 陷入死循环
我见过最典型的死循环是:模型不断调用同一个失败工具,拿到错误后又用同样的参数重试,转了五六圈毫无进展。这种问题光靠 max_steps 只能止损,不能根治。
根治的办法有两个。一个是在工具结果里带上明确的"失败原因归类",比如"权限不足"和"参数错误"要区分,模型更容易找到改方向。另一个是在调度层加循环检测,如果检测到连续多次相同工具、相同参数,就强制中断并提示用户需要人工介入或换方案。
场景三:上下文越滚越大,费用失控
多轮 agent 对话如果不控制上下文,token 消耗会呈线性甚至超线性增长。尤其是工具返回的大段 JSON,会迅速把上下文窗口占满。
解决办法就是前面说的三级记忆策略。同时要在观测层监控每轮 token 消耗,发现异常增长时及时告警。成本问题不是上线后才考虑的,而是从第一天就要记账。
场景四:工具报错信息没有回传模型
很多初版实现会在工具执行异常时直接返回"工具执行失败"这样一句废话,模型没有足够信息去修正,只能反复试错或者干脆放弃。正确做法是把异常类型、错误码、关键信息都放进回传给模型的内容里,让模型知道下一步该怎么调整。
4.2 排查方法论与日志设计
Agent 调试和传统代码调试有个很大的不同:传统代码是确定性的,同样的输入基本得到同样的输出;agent 则是概率性的,同样的输入可能得到不同路径。所以排查 agent 问题时,绝不能靠"我重新跑一次看看",一定要有完整的执行记录。
我习惯的做法是,在 agent 主循环里把每一轮的关键信息都落盘,包括:request_id、session_id、当前 step 序号、调用模型名、本轮输入 messages 快照、模型返回的 tool_calls、工具执行耗时和结果、token 消耗。出问题时,把整条执行链路像回放录像一样拉出来,一眼就能看出模型是在哪一步走偏的。
日志这件事,前期多花一小时设计,后期能省上百小时。我见过太多团队出了问题只能对着屏幕干瞪眼,最后只能清空上下文重新试,能不能复现全凭运气。
4.3 一张表速查常见问题
| 症状 | 可能原因 | 解决办法 |
|---|---|---|
| 模型调用时参数名对不上 | schema 命名不规范或描述含糊 | 统一参数命名风格,完善 description 和约束 |
| 同一个工具反复失败不换策略 | 失败信息没有粒度,模型无法定位 | 返回结构化错误信息,区分错误类型 |
| 多轮后回答质量明显下降 | 上下文塞满噪声 | 启用截断与摘要,保留与任务相关部分 |
| token 消耗异常偏高 | 大段工具结果无脑回传 | 工具结果聚合或分页,按需取详情 |
| 工具执行偶发超时卡死 | 外部 API 不稳定 | 加超时控制,超时后回传错误让模型换方案 |
| 写操作被重复执行 | 模型重试机制副作用 | 写工具增加 request_id 与幂等去重 |
这张表是我在实际项目里沉淀出来的。每个问题都踩过、修过、验证过,最后浓缩成一行,希望能帮后来的人少走弯路。
5. 场景扩展与落地建议
5.1 三个典型落地场景
场景一:企业数据查询助手
把自然语言转成 SQL 或类 SQL 查询,是 agent 最容易见效的场景之一。业务同学不用学习查询语法,直接说"上个月华东区销量前十的商品有哪些",agent 负责理解意图、拼接查询条件、执行查询、把结果转成表格或摘要。
这里要特别注意权限边界:数据查询 agent 必须继承用户本身的权限模型,不能让 agent 变成越权提数工具。实现方式是在工具层透传用户身份,SQL 生成后先做权限过滤再执行,而不是让 agent 拿到整个库的查询权限。
场景二:运维告警处理 agent
运维群里的告警消息,每天几十上百条,很多是重复的。接入 agent 后,可以自动抓取告警、查询关联指标、初步判断影响范围,然后给出建议动作,并自动创建跟进任务。对于已知模式的问题,agent 甚至可以直接执行预案脚本。
这个场景对可靠性要求很高,我强烈建议初始阶段采用"建议不执行"模式:agent 只给出分析和建议,人工确认后再操作。等积累足够多的成功案例、评估准确率稳定在可接受范围后,再逐步放开部分低风险操作的自动执行权限。
场景三:工单自动分派与答复
把工单分派规则和知识库注入工具,agent 可以根据工单内容自动判断类别、优先级、分派给对应团队,同时生成初步答复建议。它不替代客服人员,但能把最重复的那部分时间省下来,让人聚焦在真正需要判断力的事情上。
5.2 权限、安全与治理
Agent 的自动化能力越强,安全治理的权重就越高。我的原则很简单:能力可以开放,权限必须收敛。
工具级权限是最小颗粒度的控制。每个工具在注册时都应该声明访问级别,比如只读查询、写操作、高危操作。只读工具可以放开给 agent 自主调用,写操作需要明确授权,高危操作必须人工审批。实现审批的方式可以是 agent 在遇到高危工具时,暂停并把请求推送到审批群,同意后才继续执行。这套流程不复杂,但能挡住大部分事故。
另外,全链路审计日志是安全底线。每条工具调用、每个参数、每次结果,都要可追溯,否则出事时连复盘都做不到。我在项目里直接把观测层的日志同步到审计存储,保留时长按业务要求配置。
5.3 上线前一定要做的几件事
上线不是把代码部署了就算完事,对 agent 来说尤其如此。我整理了一个上线前检查清单,每一条都对应真实的教训。
- 准备一份评测集,覆盖核心任务的正常路径、边界条件和失败路径,每次改动都跑一遍回归,防止修一个 bug 引出三个新 bug。
- 全链路日志和监控先打通,请求量、成功率、token 消耗、工具失败率都要能在面板上看到,否则灰度期出问题只能抓瞎。
- 设计好兜底转人工机制,agent 明确表示不确定或连续失败时,自动降级给人工处理,不要让用户对着一个不知所措的对话框。
- 设置调用限流与预算上限,防止某个异常会话把整体成本打爆。
- 小流量灰度,先从低风险用户和低风险操作开始,稳定后再放量。
这五件事做完,agent 才能算真正"可以见人"。
我个人在实际项目里最深的一个体会是,agent 类系统的复杂度和收益成正比的临界点,往往不在模型选得有多好,而在于工具层和观测层做得有多扎实。hermes-agent 这个名字提醒我的一件事是:信使可以跑得很快,但更重要的是每一次传话都要准确、有记录、可追溯。先把这句话想明白,再开始写主循环,你大概率能少走我走过的那些弯路。