自主智能体框架设计实践:从架构到工具调用与记忆管理
我最早做 hermes-agent 这个项目的时候其实起因特别朴素我想让大模型不止停留在“聊天”这个层面而是能真正替我跑任务。比如让它自己查文档、调接口、汇总数据最后给我一份可以直接用的结果。试了一圈市面上的 agent 框架要么太重要么对自定义工具的支持太别扭于是干脆自己动手写了一个。这个项目定位是“自主智能体框架”核心思路就是把大语言模型当成一个能理解任务、拆解步骤、调用工具、逐步执行的调度中枢。我把项目命名为 Hermes是因为希腊神话里他是众神的信使负责传达信息、连接各方。这个定位恰恰说中了 agent 最本质的工作它是用户与外部世界之间的信使负责把自然语言翻译成工具调用再把工具结果翻译回人话。这篇文章我会把 hermes-agent 从架构思路到代码实现完整拆一遍包括任务规划、工具协议设计、记忆管理、执行循环这几块也会把我实际开发中踩过的坑、排查的思路一起写出来。不管你是想从零搭一个自己的个人助理还是想把 agent 能力接入业务系统这篇文章都能给你一个可以直接参考的起点。1. 项目整体设计与工程思路1.1 为什么需要自主智能体而不是简单 prompt先讲一个比较常见的误区很多人觉得“让模型干活”就是写一个很长的 prompt把需求描述清楚然后模型就会自己完成。其实对于单轮、简单的问题这样做确实够用。但一旦任务变成多步骤比如“先查数据库再根据查询结果生成图表最后发一封邮件给相关人员”整条链路里每一步都依赖前一步的输出这时候单次 prompt 就完全不够了。自主智能体解决的就是这个多步骤问题。它的核心不是“生成文本”而是“执行任务”。它把一个大目标拆解成一系列子任务每一个子任务可能对应一次工具调用也可能对应一次自我推理然后在一个循环里反复执行直到任务完成或者主动放弃。我做 hermes-agent 的时候给它的定位是“带工具的推理执行器”。它不只输出想法更要把想法变成动作。每轮循环里模型先根据当前状态决定下一步要调用哪个工具、传什么参数、为什么要这么做然后由执行引擎去实际调用工具并把结果放回上下文中让模型基于新信息继续推理。这种设计天然适合自动化研究、数据分析、信息检索、流程编排这类需要多步操作的场景。它解决的痛点是大模型本身没有“手”没法直接操作外部系统而 agent 恰好是那双手。1.2 名字里藏的设计哲学信使模式Hermes 在希腊神话里不只是传话的他还是商业、旅行、竞技的守护神是神与人之间的中介。我给项目取这个名字是想明确一个设计理念agent 的职责不是替代思考而是完成连接与翻译。这个理念直接影响了架构。在 hermes-agent 里模型负责“决定”执行器负责“行动”记忆负责“上下文”工具层负责“能力”。四者各司其职互相解耦就像信使不需要自己开餐馆才能送外卖一样agent 不需要自己实现用户的业务逻辑它只需要知道有哪个工具能用、该怎么调、结果该怎么解释。这种“中介模式”还有一个好处工具可以是任意系统里的能力。你可以接一个天气接口也可以接一个内部 CRM甚至接一个命令行脚本。只要符合协议agent 就能学会使用它完全不用为每一个新工具重写框架。说实话这是我在设计过程中最坚持的一点agent 的核心竞争力不在模型本身而在它把模型与工具连接的效率和灵活性。1.3 技术选型与核心权衡技术选型上我主要纠结过三件事语言模型后端直接用 OpenAI、Claude、国产模型这类商业 API还是用开源模型本地部署。最终我选择了接 OpenAI 兼容接口的方式因为目前绝大多数模型的 API 都兼容 OpenAI 格式这样能在一套代码里无缝切换不同厂商的模型。实际跑下来用开源模型部署在本地时延迟和成本会更可控但因为目标用户普遍对“开箱即用”有更高要求默认还是走 API 模式。工具协议一开始我想用纯自然语言描述工具让模型自己猜参数后来发现太不可控容易传错字段。最终还是用了 JSON Schema 描述工具接口让模型严格按 schema 输出结构化参数。这样虽然写法上多了一层定义但换来的是极高的稳定性模型几乎不会把参数搞错。记忆方案短期记忆用会话历史长期记忆用向量数据库。短期记忆解决“当前任务上下文”长期记忆解决“跨会话的偏好和知识沉淀”。向量库我选了轻量级的实现方式优先保证单机可跑不引入太重的外部依赖。2. 核心细节解析与实操要点2.1 任务规划如何让 agent 学会拆步骤agent 能不能干活第一步要看它会不会“分解任务”。在 hermes-agent 中我没有单独写一个规划器模块而是把规划能力直接融入模型的推理循环里。每轮循环系统 prompt 会给模型一套清晰的指令分析当前目标判断是调用工具还是直接输出最终答案。这个过程跟人做事的思路其实非常像。比如让你“调研一下行业里最近三个月的重要动态”你不会一条条列完所有搜索词才动手而是先搜几个关键词看看结果再根据结果调整方向。agent 也是这样它不是一次规划完所有步骤而是“边做边想”每一步都基于现实反馈决定下一步。我给这个机制起了个名字叫“动态规划循环”。每一轮循环模型看到的信息包括用户原始目标、历史对话、之前工具调用的结果、下一步可用工具的说明。模型需要输出一个结构化的决策要么调用某个工具要么给出最终答案。这里有个很重要的工程细节你必须限制工具调用的次数上限。如果不设上限模型可能会在一个分支上无限发散不仅拖慢响应还会烧掉大量 token。我在默认配置里把 max_steps 设为 10对于大多数常见任务已经够用。如果你在做一些特别复杂的调研类任务可以手动调到 20 或 30但一定要监控耗时和成本。2.2 工具协议把“让模型学会用新工具”这件事标准化为了让模型学会一个新的 API传统做法是把 API 文档贴进 prompt让模型对着文档猜调用方式。我以前就踩过这种坑API 文档一大模型容易漏参数或者把可选参数当成必填项。hermes-agent 的做法是让每一个工具都遵循统一的 JSON Schema 描述协议。一个工具的定义包含四部分工具名称要唯一模型靠这个名字来引用工具。工具描述用一到两句说明这个工具能做什么描述要具体避免含糊。比如“查询用户订单”这种描述远好过“执行数据库函数”。parameter schema严格定义每个参数的类型、是否必填、取值范围和字段含义。执行函数真正被调用的 Python 函数。模型每轮只会输出一个“工具调用意图”包括工具名和参数对象。执行器按照 schema 做一次校验后再真正执行这样从源头杜绝了参数类型不对、缺字段这类的低级错误。我实际使用中最大的感触是工具描述写得好不好直接决定任务成功率。你把 description 写得清楚模型就很少选错工具你写得很敷衍模型就经常乱选。所以我在项目里专门留了注释提醒后续接工具的人描述里最好包含调用场景、返回内容、典型示例翻译成人话就是把工具当人一样做自我介绍说清楚自己擅长干什么。2.3 记忆设计短期上下文、长期记忆与压缩策略agent 和普通聊天机器人最大的区别之一就是它必须“记得”自己做了什么、得到了什么结果才能继续往下走。所以在 hermes-agent 里记忆系统分成三层第一层是短期记忆也就是完整的会话上下文。它可以是一个列表存储用户输入、模型决策、工具结果等所有消息。这一层非常重要因为它直接参与模型的推理所有信息都会进入上下文窗口。第二层是长期记忆用于跨会话存储。比如用户在多次对话中反复提到自己关注某类数据指标agent 可以把这些偏好固化成向量索引下次新会话开始时自动拉取相关内容作为背景知识。这一层我用了轻量向量库实现默认是本地的嵌入式存储不依赖外部服务。第三层是压缩机制。随着任务不断执行上下文会越来越大甚至超出模型窗口或者因为包含太多无关信息而让模型分心。我在实现里加了一个上下文压缩函数当历史消息接近阈值时会把早期对话做一次摘要用精简的 summary 替换原来的完整记录保证上下文始终保持在可控长度。这里有一个工程上的取舍压缩必然会丢失部分细节所以我把压缩逻辑设置成“只在必要时候触发”。在任务执行过程中工具调用的结果尽量保留原始细节只有用户与 agent 之间的闲聊类内容才优先被压缩。3. 实操过程与核心环节实现3.1 最小可运行的项目结构先给你看一个最小可运行的 hermes-agent 项目长什么样。总共只有四个核心文件逻辑非常清晰hermes-agent/ ├── agent.py # agent 执行循环核心 ├── tools.py # 工具注册与执行器 ├── memory.py # 短期记忆与上下文管理 └── config.yaml # 模型、参数、全局配置我觉得把模块拆得足够扁反而比一上来就引入复杂的目录分层更好维护。你后续可以按需扩展比如增加 vector_store 模块或者加一个 task_queue 做异步任务但核心骨架保持简单是一个长期项目能活下来的关键。如果你想把 agent 接入业务系统你只需要在主程序中初始化一个 hermes-agent 实例注册好业务工具然后调用 run() 方法传入任务描述即可。它在设计上不是一个“应用”而是一个库这样你可以在自己的服务里灵活嵌入。3.2 agent 主循环代码实现agent 的核心就是执行循环。我用一个简化版本的伪代码来说明但代码结构是从项目里直接整理出来的class HermesAgent: def __init__(self, model_client, tools, memory, max_steps10): self.model model_client self.tools tools self.memory memory self.max_steps max_steps def run(self, task: str) - str: # 把用户任务写入短期记忆 self.memory.add_message({ role: user, content: task }) for step in range(self.max_steps): # 构建模型输入系统提示 记忆 工具描述 prompt self.build_prompt() response self.model.chat(prompt) # 模型返回 final说明任务结束 if response[type] final: return response[content] # 模型返回 tool_call说明要调用工具 if response[type] tool_call: tool_name response[tool_name] tool_args response[tool_args] # 在 schema 校验通过后再执行 result self.tools.execute(tool_name, tool_args) # 把工具结果写回记忆 self.memory.add_message({ role: tool_result, content: result, tool_name: tool_name }) # 超过最大步数时返回当前进度并提示 return 任务未在限定步数内完成请考虑调整任务描述或增大 max_steps。这个循环看起来简单但我实际开发时花了大量时间打磨细节。最大的坑在于构建给模型的 prompt。模型每轮能看到的内容必须非常精确既要完整展现之前已经执行过的步骤又不能把无关信息塞进去。我在工具调用结果前面加了一个前缀描述“这是调用某工具后拿到的原始输出”这样模型在下一次推理时能清晰区分什么信息是真实的、什么信息是它自己推出来的。另外模型跟模型的行为差异很大。有些模型在不给明确格式要求时会输出自然语言段落而不是结构化的 JSON所以我在 system prompt 里严格规定了 response 的 JSON 结构并且加了“不要输出任何解释直接输出 JSON”这样的指令。实测下来这条指令能显著降低解析失败率。3.3 自定义工具的注册与接入任何 agent 框架的易用性都取决于接入新工具的难度。hermes-agent 采用装饰器注册机制你在一个 Python 函数上标注 tool写上工具的 meta 信息它就自动进入工具库可以被模型发现并调用。下面是一个实际从项目里摘出来的工具注册示例from hermes.tool import tool tool( namesearch_knowledge_base, description( 在本地知识库中检索与关键词相关的资料。 适用于查找技术文档、FAQ、历史决策记录等场景。 参数 query 为要检索的关键词建议用简洁核心短语长度不超过 20 字。 ), parameters{ type: object, properties: { query: { type: string, description: 检索关键词例如用户反馈、登录报错 }, top_k: { type: integer, description: 返回前 k 条相关内容默认 5, minimum: 1, maximum: 10 } }, required: [query], additionalProperties: False } ) def search_knowledge_base(query: str, top_k: int 5) - str: # 这里是你的具体检索实现 results do_vector_search(query, top_k) return \n.join( f[相关度 {score:.2f}] {content} for content, score in results )接入过程非常简单你只需要在初始化 agent 之前注册好所有工具agent HermesAgent( model_clientget_model_client(), toolsload_tools(), memoryMemory() ) agent.run(本周有哪些用户反馈与支付相关请整理成摘要)我特别想提一个细节parameters 里设置 additionalProperties: false 会很有用。它能让模型严格只输出 schema 里定义过的字段避免模型自由发挥塞一些未知参数导致校验失败。这个字段在很多工具协议里被忽略但它实际上能帮你压掉不少错误。3.4 配置示例与运行效果配置我用 YAML 文件保存方便按环境调整model: provider: openai_compatible base_url: http://localhost:8000/v1 api_key: sk-xxx model_name: qwen2.5-7b-instruct temperature: 0.2 max_tokens: 2048 agent: max_steps: 10 system_prompt: | 你是 Hermes一个能调用工具解决问题的智能体。 请根据工具返回的实际结果逐步推理不要编造。 如果工具结果不足就继续调用合适工具只有确认任务完成才输出最终答案。 你每一步必须输出 JSON结构如下{type: tool_call 或 final, ...} memory: max_message_history: 40 enable_summarization: true summarize_threshold: 25模型推理相关参数我调得偏保守temperature 设在 0.2保证 agent 每一步决策尽量稳定不会因为随机性太大而在同一个问题上反复换方案。如果你希望 agent 在创意类任务上更有发散性可以调高到 0.7 甚至 1.0但多步执行任务我不建议超过 0.3。实际运行效果方面我用它做了一轮“调研三篇关于向量数据库的技术文章总结各自优缺点”的任务。agent 的轨迹大致是调用 search_knowledge_base 搜“向量数据库”拿到一批文章标题和摘要再根据结果逐个提取需要深入阅读的标题又调用一次检索工具逐篇获取详情最后综合内容输出对比表格。整个过程一共用了 6 步约 40 秒输出质量相当不错。4. 常见问题与排查技巧实录4.1 模型陷入死循环反复调用同一个工具这是我在开发过程中遇到最多的问题。模型多次调用同一个工具、传几乎一样的参数说明它没有从工具结果里学到新信息可能不知道该干嘛了。我的排查思路是先打开日志看每一轮模型输入和输出确认模型每次拿到的工具结果是不是真的足够解决问题。如果工具结果本身信息量不大模型就会陷入“不知道下一步”的循环。解决方案有两种。一是给工具返回内容写得更加结构化和完整让模型有足够多的线索做下一步推理。二是在 system prompt 里加规则“如果上一步工具返回的结果无法推进任务尝试换一个工具或角度不要重复调用同一个工具超过两次。”代码层面我也做了兜底执行器里记录每个工具最近三次的调用参数如果出现完全相同的连续调用直接中止循环并返回人工介入提示。这个设置看似简单但能帮你省下大量 token 浪费。4.2 工具参数频繁解析失败模型传错字段这个问题高发于接第三方 API 时。第三方文档字段常叫 weird_name我本地函数参数叫 pretty_name模型很容易在两者之间产生映射错误。我的解决方案是双保险。第一层工具 schema 里字段名称尽量贴近自然语言习惯同时用 description 注明字段别名和使用样例。第二层在 execute 之前加一层参数校验和转换允许定义轻量的 transform 函数把模型传入的原始字段转换成本地函数需要的精确结构。注意这里不要过于迷信 prompt 指令模型对字段名的敏感度远高于对描述的敏感度能改字段名就别指望模型自己理解。另外如果你的工具很多模型会更容易搞混。一种有效做法是给每个工具的 description 开头加上业务领域标签比如“[财务] 查询对账状态”“[客服] 获取用户退款记录”实测能明显降低选错工具的概率。4.3 长任务执行时上下文膨胀响应越来越慢agent 每一步都会把新的工具结果追加到上下文里几十步以后输入 token 动辄上万不仅响应变慢模型也容易“迷失”在冗长的历史里忘记最初的目标。我这里提供三个思路结合起来用效果最好。一是滑动窗口只保留最近 N 轮对话和工具结果早期的内容压缩成一段摘要。二是关键信息抽取每当工具返回较长的原始数据时在写入记忆之前先做一层精简只保留对后续任务可能有用的关键片段。三是全局目标锚定每一轮系统 prompt 里重复一遍用户最初的任务描述让模型时刻知道“当前动作是为了完成这个目标”而不是被中间结果带偏。我特别推荐第三种思路因为它实现成本极低效果却立竿见影。有一次测试中我发现模型在第五步时已经完全忘了最初要比较三个方案而只是盯着最新拿到的某篇文章大谈特谈加入全局目标锚定之后这个问题几乎消失。4.4 模型产出幻觉内容工具结果没覆盖就乱编大模型本身有幻觉倾向在 agent 场景里这种倾向会被放大因为 agent 要被要求“给结论”当工具结果不足时模型可能会强行补全一个看起来合理的答案。我在系统配了一个指令“如果当前工具结果不足以支撑最终结论请明确回复信息不足并列出你还需要获取哪些信息不得编造数据。”同时代码里也对工具调用次数做了约束如果任务最终走到了 max_steps 却还没有拿到可支撑结论的数据宁可返回“未完成”状态也不能让模型硬编一个答案出来。如果你做的是数据敏感型任务还有一个更可靠的做法不做人工抽检的话禁止 agent 输出具体数字强制要求它引用工具结果的原文段落作为依据。模型只有在 response 中包含“依据引用”时最终结果才被允许返回给用户否则直接拦截。4.5 常见问题速查表现象可能原因解决方案agent 反复调用同一工具工具结果信息量不足模型无法推进优化工具返回内容限定重复调用次数工具参数频繁传错schema 字段名不够直观或描述不清修改字段名增加 sample 示例加 transform上下文太长响应慢历史积累过多启用压缩、滑动窗口、关键信息抽取模型编造结果工具结果不足以支撑结论强制引用原文设置信息不足时的返回逻辑模型频繁选错工具工具描述含糊功能混淆在描述前加领域标签写清使用场景agent 突然退出循环模型输出格式解析失败检查 prompt 里的 JSON 格式说明增加重试机制多个用户任务混淆短期记忆没有按会话隔离每轮任务单独创建记忆实例完成后销毁5. 行业影响与可扩展方向5.1 agent 框架对个人效率的深层影响我做了 hermes-agent 之后最大的感受是agent 带来的不是“某一件事变快了”而是“一个人能管理的事情变多了”。以前做行业调研我要自己开搜索引擎开文档工具把多方信息粘来粘去再梳理结构。现在我把“搜索、整理、对比、生成报告”这条链路交给 agent自己只做两件事定义目标和审查结果。这相当于把执行层剥离出去了而人的精力被释放到更高价值的判断层。这种变化对独立开发者、研究者、运营人员这类经常需要跨系统处理信息的角色特别明显。你不需要会写多复杂的代码只需要清楚描述自己想要什么agent 会帮你完成中间那些繁琐的检索、组合、筛选动作。这不是替代而是把人的能力半径扩大了一圈。5.2 从个人工具到团队基础设施给业务系统带来的改变把 agent 接到团队业务系统里是一个更大的话题但同样可以从小切口开始。比如客服场景传统机器人只能按关键词回答固定问题而接入 agent 的客服可以做到读用户描述查订单系统看物流状态必要时生成一封处理建议交给人工复核。这个过程中底层基础设施的要求会变高。你需要保证工具层的稳定和权限可控需要让 agent 能理解你们业务里特有的一些黑话也需要有人维护工具文档和错误返回信息。但有一个好消息是agent 对存量系统的侵入性其实很小因为它通过 API 和工具协议接入不需要改业务核心代码。我在设计 hermes-agent 时坚持“工具即协议”的路线也是基于这个考虑。当你要把 agent 嵌入一个已经运行多年的业务系统时最稳妥的方式不是让 agent 直接操作数据库、直接写业务表而是先封装一层服务化工具让 agent 只能通过授权接口操作。这样风险可控也方便审计。5.3 多 agent 协作与复杂任务调度如果你觉得单 agent 的能力还不够下一步自然就是“多 agent 协作”。我自己也正在做这样一个实验一个 Planner agent 负责拆任务几个 Worker agent 分别负责搜索、计算、写作再由一个 Reviewer agent 汇总审查。它们之间通过一条简单的消息队列传递任务和结果。这个方向有几个绕不开的问题任务如何分片、各 agent 之间的数据依赖怎么表示、结果冲突怎么仲裁、消息怎么收敛。我目前的做法比较土但有效就是用“任务清单 结果表”共享状态Planner 生成子任务清单Worker 认领后把结果写入结果表Reviewer 最后读取汇总。尽管还很原始但已经能完成一些需要多角色协作的复杂流程。5.4 后续可以怎么扩展hermes-agent 目前的重点还是“单任务、单轮执行、可复现的确定性输出”。后续我计划做这几个方向定时任务与触发器让 agent 可以订阅事件比如每天凌晨自动汇总前一日运营数据或者监控某个数据指标超过阈值时自动告警。更细粒度的权限控制不同角色注册不同工具agent 在什么场景下能用什么工具都做成可配置策略。人机协同模式agent 在不确定的时候主动向用户提问而不是硬着头皮继续走这能把任务成功率提高一大截。更完整的评测体系用一批标准任务定期回归测试防止模型升级或者工具调整导致整体能力回退。我觉得这个方向会越做越深但有一条主线是确定的agent 的目标永远是“可靠地完成任务”而不是“看起来聪明”。所有技术选型、模块设计、工具协议都应该服务于这个最朴素的目标。最后再分享一个很实际的小技巧agent 任务开始前先用一句话在 system prompt 里给模型“定调子”例如“你是一个严谨的研究员输出前必须核查依据”会明显影响整条轨迹的质量。很多人忽略了这个细节总在工具层找问题其实模型的心理暗示也很重要。