最近一两个月,我一直被一个问题困扰:手头的自动化工具越来越多,但每个工具都是孤岛。管 Git 仓库的只管仓库,发通知的只管通知,汇总文档的只会汇总文档,想让它们协作完成一件事,就得自己写一堆胶水代码来回传数据。直到我认真研究并搭建了 hermes-agent 这类个人智能体项目,才真正体会到"传话人"的价值——它就像团队里的协调员,把大模型的能力和一堆离散工具串成一条完整的流水线。
这篇文章我会从 hermes-agent 能解决什么问题讲起,逐步拆解它的核心架构,然后给出一套可以直接落地的搭建方案(附带踩坑记录和排查思路),最后分享几个让 Agent 更聪明的调优技巧。适用对象是那些已经接触过 Python 和基本 API 调用、但对 Agent 编排还停留在概念层面的开发者;如果你完全没写过代码,也能通过前面两章理解它的工作逻辑,后续章节可以直接"抄作业"。
1. 它到底解决什么问题:多个工具之间的"传话人"
先说个具体场景。我团队每周都要出一份项目周报,传统流程是这样的:登录 Git 平台看提交记录、打开任务管理后台导出进度、把关键数据手动粘到文档里排版、最后复制到企业微信群。这套流程每次要花四十分钟左右,而且极其机械。更难受的是,这些操作分布在四五个系统里,互相之间没有任何数据通道。
hermes-agent 这种项目的思路,就是在这些系统之上加一个"调度中枢"。它本身不直接产生业务数据,但能听懂你用自然语言下达的指令,然后把指令拆解成一系列子任务,再为每个子任务挑选合适的工具去执行,最后把所有结果汇总成你想要的格式。整个过程就像你雇了一个能听懂人话的实习生,他手里拿着各个系统的操作手册,你把需求说清楚,他自己判断该翻哪本手册、按照什么顺序执行、最后怎么把结果交给你。
名字也很传神。Hermes 是希腊神话里的信使神,专职传递消息、引导行程。这个项目取这个名字,定位非常明确——它是工具链里的信使和调度者,而不是越俎代庖的业务系统。理解这一点对后面的使用很重要:你不需要让它接管所有数据,只需要让它能在正确的时间、用正确的方式,把正确的数据送到正确的地方。
和传统的自动化脚本对比,它的优势很明显:
| 维度 | 传统脚本 | hermes-agent 这类智能体 |
|---|---|---|
| 需求表达 | 需要精确到每一步的指令 | 用自然语言描述目标即可 |
| 异常处理 | 按既定分支执行,遇到意外容易中断 | 能根据中间结果调整后续步骤 |
| 工具扩展 | 改脚本逻辑,代码侵入性强 | 增加一个工具描述文件即可 |
| 结果整合 | 需要自己写拼接逻辑 | 由模型汇总成结构化报告 |
| 适用场景 | 流程固定、输入输出明确的重复任务 | 流程多变、需要灵活判断的复合任务 |
它不适合做什么,我也得说清楚。如果你只是想把 A 文件夹里的文件备份到 B 文件夹,用 crontab 加一条 rsync 命令就够了,让 Agent 上阵纯粹是杀鸡用牛刀。如果任务需要极高的数值精度(比如财务对账),也不建议把核心计算交给大模型,模型在数学计算上仍然可能出错。最合适的场景是:需要跨系统取数、需要综合判断、结果允许一定程度的非精确性、步骤可以灵活调整的中间层任务。
2. 架构怎么搭:调度中枢的四个核心模块
我搭完一套 hermes-agent 之后复盘,发现它的核心架构可以拆成四个模块:大脑、工具集、规划器和记忆层。搞清楚这四个模块各自的职责和协作方式,你就能理解市面上大多数 Agent 框架的设计逻辑,也能在遇到问题时快速定位是哪个环节出了岔子。
2.1 大模型大脑:选对模型比调 prompt 更重要
大脑就是接入的大语言模型,负责理解用户指令、判断该调用什么工具、解析工具返回结果、生成最终回复。在 hermes-agent 这类项目里,大脑依赖的关键能力是 function calling(函数调用),即模型在生成回复时,不只是输出文字,还能输出结构化的"调用某工具及对应参数"指令。
实测下来,不同的模型在 function calling 上的表现差异很大。有些小参数模型即使微调过,也经常出现"幻觉式调用"——能说出工具名,但给出的参数和工具定义的 JSON Schema 对不上,导致下游解析直接报错。我现在的主力配置是:规划决策用能力较强的通用模型,简单抽取任务用响应更快的轻量模型。如果你的项目允许自定义模型接入,建议至少用一个主流旗舰模型做兜底,否则后续调工具的崩溃率会消耗掉你所有耐心。
这里有一个容易忽略的细节:同样一个模型,在不同的 temperature(采样温度)参数下,function calling 的稳定性也不同。做工具调用调度时,我习惯把 temperature 调低到 0 到 0.3 之间,让模型不要那么"有创意",严格按照格式输出。一旦温度高了,模型就会开始编造不存在的参数。这个参数在后面的翻车环节还会再提到。
2.2 工具注册表:让 Agent 知道手里有什么牌
工具注册表是连接大脑和实际系统的桥梁。每个工具都被描述成一个结构化的 JSON Schema,包含工具名称、功能描述、入参定义、返回值格式。模型看到这些描述后,才能做出"当前任务应该调用哪个工具"的决策。
我维护工具注册表的一条重要经验是:描述信息直接影响调用准确率。你写"git_log: 获取 git 提交日志",模型可能会在被问"最近代码有什么改动"时犹豫是否调用;但如果你写"当用户询问最近提交记录、代码变更、开发进度、或者需要汇总本周工作内容时,调用此工具获取仓库提交历史",模型就会在更多相关场景主动调用。工具描述本质上是在教模型做"意图匹配",这部分内容值得反复斟酌。
每个工具的入参定义也要尽量收敛。参数越少、类型越明确,模型出错的概率越低。拿"发送企业微信消息"这个工具举例,我一开始定义了收件人、消息标题、消息正文、消息类型、是否@所有人、链接地址等十来个字段,结果模型经常漏填或者填错。后来我精简成三个必填字段(收件人、标题、正文),类型全是字符串,调用成功率立刻上去了。工具是给模型用的,不是给用户用的,设计时要想"模型好不好理解"而不是"人好不好用"。
2.3 任务规划器:分步拆解还是边做边想
规划器解决的核心问题是:一个复杂任务应该按什么顺序执行。这个模块有两种典型实现方式,各有利弊。
第一种是 Plan-and-Execute 模式:任务开始时,先让模型基于当前可用工具生成一个完整的执行计划,然后按计划逐步执行。这种方式的好处是全局视角好,用户能从一开始就看到 Agent 准备怎么干;坏处是如果中间某一步的结果和预期偏差太大,后面计划可能全盘作废。
第二种是 ReAct 模式的变体:模型每执行一步,都会观察工具返回结果,再决定下一步做什么。这种方式灵活,遇到意外能当场调整,但容易出现"跑偏"——尤其当上下文很长时,模型可能忘了最初的目标,越走越远。
我目前的实现是两者结合:先让模型生成粗略计划,但在每步执行前重新评估"这一步是否符合大目标",如果偏离就修订计划。简单说就是"有计划但不死板"。你在自己的项目里如果不想做这么复杂,可以先用纯 Plan-and-Execute 跑固定流程类任务,等稳定了再引入动态调整。
2.4 记忆层:分清临时便签和长期资料库
记忆层负责存两样东西:当前对话里的临时上下文和跨会话的长期信息。前者通常直接拼在 prompt 里,后者一般用向量数据库存储,通过语义检索在需要时把相关片段取出来。
我在初期犯过一个典型错误:把所有工具返回值全部塞进上下文,导致模型几轮之后就被海量日志淹没了。后来我加了"信息筛选"这一步——工具返回长文本时,先让轻量模型做摘要提炼,只把精简版给主模型看。这一招对控制 token 消耗和提升主模型判断质量都有奇效。
长期记忆则要克制。不必把每次对话都记下来,而是有选择地保存"用户偏好"和"关键事实"。比如用户说过"周报发送前必须过一遍人工确认",这条应该进长期记忆;某次任务里的某个中间计算数值,就不需要记。记多了反而会让检索结果变杂,影响模型的回答质量。
3. 从零跑通第一个 Agent:搭建一个项目周报助理
有了理论铺垫,下面直接用代码过一遍完整流程。我做了一个"项目周报助理":用户丢给它一句话"汇总本周的 Git 提交并发送到企业微信群",它能自己完成取数、整理、发送三个动作。这个例子麻雀虽小但五脏俱全,包含了 Agent 工作的完整链路。
3.1 准备阶段:目录结构和依赖
先建一个干净的目录结构,我习惯按功能分模块:
hermes-agent/ ├── agent/ │ ├── __init__.py │ ├── core.py # 主循环逻辑 │ ├── planner.py # 任务规划器 │ └── memory.py # 记忆管理 ├── tools/ │ ├── __init__.py │ ├── git_tool.py # Git 提交记录工具 │ ├── notify_tool.py # 企业微信通知工具 │ └── registry.py # 工具注册表 ├── config.py # 配置文件 ├── requirements.txt └── main.py # 入口依赖方面,核心需要两个包:一个用来调用大模型 API,一个用来管理配置。如果你用的模型服务商有自己的 SDK,直接用官方 SDK 也完全可以。requirements.txt 里我一般让大模型 SDK 的版本保持最新稳定版,因为 function calling 相关的接口迭代很快,旧版本可能不支持某些新参数。
3.2 注册第一个工具:获取 Git 提交记录
工具代码本身不复杂,核心是把执行函数和描述信息绑定。下面是 git_tool.py 的精简实现:
import subprocess from datetime import datetime, timedelta def get_git_log(since_days: int = 7) -> str: """获取指定天数内的 git 提交记录。""" since = (datetime.now() - timedelta(days=since_days)).strftime("%Y-%m-%d") cmd = ["git", "log", f"--since={since}", "--pretty=format:%h|%an|%ad|%s", "--date=format:%m-%d %H:%M"] result = subprocess.run(cmd, capture_output=True, text=True, cwd="/path/to/your/repo") return result.stdout if result.returncode == 0 else f"执行出错: {result.stderr}" # 工具注册描述,模型靠它理解什么时候该调用 GIT_LOG_TOOL = { "type": "function", "function": { "name": "get_git_log", "description": "获取指定仓库最近 N 天的提交记录,包括提交人、时间和提交说明。当用户询问代码进展、提交记录、开发内容时使用。", "parameters": { "type": "object", "properties": { "since_days": { "type": "integer", "description": "需要回溯的天数,默认 7 天", } }, "required": [] } } }这里有个关键点:description字段不是写给人类看的,而是写给模型看的。文字里出现的触发场景越多,模型越容易在合适的时机选中它。我后面调优时发现,把常见问法直接写进 description 里(比如"本周进展"、"最近改动"、"代码提交"),召回率明显提升。
3.3 注册第二个工具:发送企业微信通知
发送通知的工具同理,我用的是企业微信机器人 webhook,只需要一个 URL 就能发消息,非常轻量。
import requests def send_wecom_message(title: str, content: str) -> str: """发送消息到企业微信群机器人。""" webhook_url = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY" payload = { "msgtype": "markdown", "markdown": { "content": f"### {title}\n{content}" } } resp = requests.post(webhook_url, json=payload, timeout=10) return resp.text WECOM_TOOL = { "type": "function", "function": { "name": "send_wecom_message", "description": "向企业微信群发送 markdown 格式消息。当用户要求发送通知、周报、提醒到企业微信时使用。", "parameters": { "type": "object", "properties": { "title": {"type": "string", "description": "消息标题"}, "content": {"type": "string", "description": "消息正文,支持 markdown 格式"} }, "required": ["title", "content"] } } }webhook 这种接入方式我很推荐用在 Agent 工具里:不需要额外认证逻辑,一个 URL 就能搞定,非常适合内部工具。如果你要对接的是更正式的 IM 应用,可以换成官方 API,但工具封装逻辑是完全一样的。
3.4 主循环:让 Agent 自己决定怎么干活
核心主循环的逻辑其实不复杂:把用户指令、可用工具、历史记录发给模型,模型返回"直接回答"或"调用工具"两种结果,如果是后者就执行工具并把结果喂回去,循环直到模型认为任务完成。下面是 core.py 的主循环伪代码:
def run_agent(user_input: str, tools: list, max_steps: int = 10): messages = [{"role": "user", "content": user_input}] for step in range(max_steps): response = llm.chat( messages=messages, tools=tools, temperature=0.2 ) message = response.choices[0].message # 模型决定调用工具 if message.tool_calls: messages.append(message) for tool_call in message.tool_calls: result = execute_tool(tool_call) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": result }) continue # 没有工具调用说明任务已经完成 return message.content注意几个细节:max_steps一定要有,防止 Agent 陷入无限循环;temperature设置为 0.2,目的是让工具调用格式尽量稳定;每次工具调用的结果都会以 role 为 "tool" 的消息追加进上下文,模型在下一轮才能"看到"工具返回了什么。
另外一个实用技巧是:在系统提示词里写清楚"当工具返回空数据时,不要猜测,直接告诉用户没有查询到相关记录"。没有这条约束,模型经常会在数据为空时编造一些看起来很合理的内容,这是最需要警惕的幻觉来源。
3.5 跑起来看效果
把入口脚本写好之后,运行命令:
python main.py "把最近三天的 git 提交整理成周报,发到企业微信群"Agent 的完整执行过程是这样的:
- 第一轮:模型分析任务,判断需要调用
get_git_log,参数since_days=3,随后执行工具拿到提交列表。 - 第二轮:模型看到提交记录,把它们整理成 markdown 格式的周报,判断需要调用
send_wecom_message,随后执行发送。 - 第三轮:模型确认消息发送成功,返回"我已经把最近三天的提交记录整理并发送到了企业微信群。"
整个链路不到 20 秒,中间没有写一行胶水代码。主体的数据拼接和组织工作,全都是模型根据工具返回内容自行完成的。这就是 hermes-agent 类项目和传统脚本最大的体验差异:你只需要描述目标,不用描述路径。
4. 实测中的翻车现场:四个高频问题与排查链路
跑通 demo 只是开始,真正让一个 Agent 稳定可用,得经历大量排障。下面四个问题是我测试和使用过程中出现频率最高、最让人头疼的,我把完整的排查链路写出来,希望你遇到时不至于从头摸黑。
4.1 工具调用格式不稳定:模型不按 JSON 出牌
现象:模型有时候返回的 tool_calls 字段不是结构化对象,而是把调用信息写在普通文本里;或者 JSON 不合法、参数名和 schema 对不上。
排查顺序:先看模型原始输出,确定是"完全不知道该输出 JSON"还是"知道但格式错了"。如果完全不知道,检查请求里是否真的带上了 tools 参数,这一问题常见于 SDK 版本过旧或者参数名拼写错误。如果是格式错了,重点看 temperature 是不是太高,我把它从 0.7 调到 0.2 后这类问题少了一半以上;再看工具参数定义是不是太复杂,比如有多层嵌套结构,模型很容易在嵌套上出错。
我的最终方案是双保险:解析 tool_calls 时统一走一个容错函数,先尝试标准解析,失败后用正则提取函数名和参数片段再二次解析。这个容错层虽然不优雅,但在生产环境里非常管用。
4.2 死循环:Agent 反复调用同一个工具
现象:Agent 拿回结果后莫名其妙继续调用同一个工具,或者在某两个工具之间来回横跳,直到 max_steps 耗尽。
我抓到一个典型案例:让它查某个仓库的提交记录并生成进度报告,它拿到结果后没有直接整理发送,而是又调用了一次 get_git_log,第二次的返回和第一次一模一样,它还是继续调用。看日志发现,问题出在模型上下文里的信息过于冗长,它可能"忘了"自己已经拿到结果。
排查思路分三步:第一步看 max_steps 设置的阈值,我一开始给到 20,后来发现最复杂的任务 8 步以内也能完成,超出 8 步极大概率是出问题了,直接截断并返回部分结果,同时打印告警日志;第二步检查工具返回内容是否过长,长文本会干扰模型对当前状态的判断,解决方法是让工具自己先对结果做摘要,只保留关键信息;第三步在系统提示词里加一句"如果再次调用工具前,需先确认上一步获得的数据是否已满足用户需求,如果满足就直接汇总输出"。这句提示对抑制重复调用效果显著。
4.3 参数传递错位:字符串和结构化对象的传统矛盾
现象:工具定义要求since_days是数字,模型传成了字符串 "3";或者应该传数组的传成了逗号分隔的字符串。
这个问题的根源在于大模型对 JSON 类型敏感度不足。排查时先看工具执行端的日志——我最初实现 execute_tool 时直接把模型给的参数当最终值,结果字符串类型的数字在 git 命令里也凑合能跑,掩盖了问题。后来我改成在 execute_tool 入口做一个参数归一化:把字符串形式的数字转成 int,把逗号分隔的字符串按 schema 定义转成数组,实在无法转换的再报错。这个归一化层能消解大部分类型错位。
但我后来发现,过度宽松的参数转换也有副作用:模型会越来越"懒",依赖你给它兜底。所以最佳策略是:归一化逻辑保留,同时在传给模型的工具描述里增加"参数类型必须严格匹配定义"的提示。宽容处理和严格提示并存,出错率能降到可接受范围。
4.4 上下文被塞满:长日志和重复数据拖垮模型
现象:任务执行到一半,模型响应速度明显变慢,回答质量下降,甚至开始答非所问。查看请求体发现,上下文字符数已经到了几十万级别。
根因有两个:一是工具返回内容没有做裁剪,第一次拿到的完整日志几十 KB 全部进上下文;二是多轮对话保留策略太宽松,历史消息全部累积。我的处理方式分两层:第一层是工具返回内容动态截断,超过 3000 字符就调用轻量模型做摘要;第二层是上下文窗口管理,每隔几轮把早期消息压缩成一条摘要,只保留最近几轮的完整消息。压缩后的会话仍然能满足模型处理任务的上下文需求,同时把 token 消耗降了一个量级。
如果你用的是带自动摘要的模型服务商,可以省去自己写压缩逻辑的功夫,但要留意摘要的质量——信息丢失太严重同样会让 Agent"失忆"。
5. 调 prompt 和扩展工具的实战经验
从"能用"到"好用",中间隔着大量 prompt 调优和工具扩展工作。这一章分享几个我实践过多次、确实有效的经验。
5.1 工具描述怎么写:给模型一本"操作字典"
我总结了一套"工具描述四要素":
- 功能定义:这个工具是干什么的,一句话说清楚。
- 触发场景:用户在什么意图时应调用它,列出至少三个典型问法。
- 边界条件:什么情况下不应该调用它,这能减少误调。
- 示例参数:给一个完整的调用示例,让模型模仿格式。
下面是我优化后的一个工具描述片段:
{ "type": "function", "function": { "name": "search_docs", "description": ( "在内部文档库中搜索相关知识。" "当用户询问操作步骤、产品功能、配置方法、错误码含义时使用。" "如果用户问的是代码报错排查,优先考虑本工具而不是直接猜测。" "不要用这个工具查询天气、汇率等无关信息。" "参数示例: {'query': 'webhook 配置说明', 'limit': 5}" ) } }模型对示例的模仿能力很强,给一个正确示例,比反复强调格式要求更管用。需要提醒的是,描述不是越长越好,超过 200 字反而会让模型抓不住重点,尽量控制在 100 到 150 字之间。
5.2 系统提示词:给 Agent 立规矩
系统提示词是指导模型整体行为的纲领。我目前的版本重点包括这几条:
- 只基于工具返回数据回答,不编造不存在的记录。
- 工具返回为空时明确告知用户,不强行填充内容。
- 涉及高风险操作(发送消息、修改数据、删除文件)前,先向用户确认。
- 如果用户指令不清楚,先追问澄清,不主观臆断。
- 执行结果要简明扼要,给出关键信息即可,不堆砌原始数据。
关于高风险操作确认,这一点我强调再多也不为过。我让 Agent 发消息给测试群时,因为没加确认机制,某一版 prompt 下模型把定时任务理解错了,差点把内部测试消息发到生产环境群。加上确认环节后,虽然在便利性上打了一点折扣,但安全性好了太多。生产环境里哪怕多一步人工确认,也值得。
5.3 新增工具时最容易忽略的两个点
第一个点是负载和超时。Agent 调用工具是有超时上限的,如果某个工具执行要几十秒,模型那边可能已经超时。我有个工具要去爬外部网站,经常要跑 30 秒以上,后来改成异步提交任务再轮询结果,工具执行接口立刻返回"任务已受理",Agent 后续再调用查询接口拿结果。
第二个点是工具之间的关联提示。当两个工具常常配合使用时,可以在各自的 description 里互相提一句。比如在 get_git_log 的描述末尾加上"获取到提交记录后,用户可能还需要将内容发送到企业微信,可以配合 send_wecom_message 使用",模型在规划时就更倾向于组合调用这两个工具。
5.4 进一步的方向:多 Agent 协作
单个 Agent 工具多了以后,工具选择的准确率会下降,毕竟模型要在几十个工具里挑合适的。一种解决思路是拆分多个专业 Agent,每个 Agent 只负责某类工具,由一个主 Agent 做路由仲裁。比如"数据查询 Agent"管所有取数工具,"消息推送 Agent"管所有通知工具,"文档处理 Agent"管所有格式转换工具。
我目前的实践是主 Agent 收到请求后,先判断任务类别,转发给对应的专业 Agent,再由专业 Agent 调用具体工具。这样每个 Agent 的工具列表都只有 3 到 5 个,调用准确率比一个 Agent 挂 20 个工具高很多。代价是需要管理 Agent 之间的通信协议,复杂度上升了一个层级。如果你处理的业务一次性任务不超过 5 个工具,先用单 Agent 就够了,别急着上多 Agent 架构。
6. 落地过程中的几条真实建议
6.1 日志先行:没有日志你就失去了双眼
Agent 的不可控性比传统程序高不少,每次调用的请求参数、模型返回值、工具执行结果都必须落盘。我一开始只打印控制台日志,后来发现排查问题时根本不够用——你看不到历史某个时刻的完整上下文。现在的做法是每次运行生成一个带时间戳的日志目录,里面包含完整的 messages 数组、每步工具调用的参数和返回结果。排查问题时直接按时间线回放,效率比猜高得多。
6.2 从确定性高的工具开始
不要一上来就部署一堆外部接口型工具,先把本地的、可模拟的、返回结果容易验证的工具跑稳。我建议第一个 Agent 先只做一个文件操作工具,比如读取指定目录下的文件名列表。这个工具不需要外部依赖,结果直观,你可以快速验证整个调用链路是否通畅。外部 API 型工具建议等链路稳定后再陆续加进来,每次只加一个,方便定位是新增工具的问题还是原有链路的问题。
6.3 人工确认环节不能省
Agent 自动执行和业务安全之间需要一条缓冲带。我的做法是所有外部副作用操作(发消息、发邮件、改数据库、调第三方 API)默认带一个 dry_run 模式,实际执行前先打印将要执行的动作和参数,确认无误后才真正下发。等对 Agent 的行为模式足够熟悉、且积累了充分的测试用例之后,再逐步放开部分低风险操作的自动执行权限。
6.4 配置和版本管理
Agent 的行为依赖于系统提示词、工具描述、模型参数、工具代码四部分。这四部分任意一个变化,都可能导致行为改变。我建议把这些配置全部纳入 Git 管理,并且每个版本的变更都要记录效果。有一次我把系统提示词里的一句话从"务必返回简洁的结果"改成"返回简洁的结果",结果模型的输出风格大变,开始在回答里加入大量额外说明。如果没有版本记录,你根本不知道是哪次改动引起的。
用 hermes-agent 这类项目,本质上是把决策权的一部分交给了模型。你要做的不是完全信任它,而是给它足够的边界、完善的日志、和清晰的反馈机制。在这个前提下,它能帮你省下的时间确实非常可观——我现在每周的例行报告,基本就是一句指令的事,剩下的时间用来干真正需要人参与的工作。