ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

从零实现AI Agent:hermes-agent架构设计与200行核心代码

从零实现AI Agent:hermes-agent架构设计与200行核心代码 被 ChatGPT 惯坏之后我一直有个执念模型已经能写代码、能总结文档、能对答如流可我想让它顺手把这件事做完它却总是停在好的你可以这样做这一步。后来我决定自己动手做一个项目代号就叫hermes-agent。Hermes 在神话里是众神的信使负责传令、引路、跑腿我要做的这个 agent职责也一样听懂目标拆解任务调用工具闭环交付。这篇文章不是讲某个开源框架怎么装也不是列一堆概念名词而是把我从零设计 hermes-agent 的真实过程摊开来讲整体架构怎么分、规划器怎么实现、工具调用有哪些坑、记忆系统怎么取舍、安全边界画在哪。最后还会给出一版 200 行内的最小可运行实现让你能照着动手把流程跑通。适合所有正在做 AI Agent 项目的开发者尤其是那些已经跑通过 demo、想把它做成真正能干活的产品的人。1. 为什么做 hermes-agent从能聊到能办事的最后一公里先说一个很多人都有的体验你和 GPT 说帮我分析一下这份销售数据找出增长最快的区域它能给你讲得头头是道但它不会真的去读你本地的 Excel不会自己写 Python 脚本更不会把结果整理成报告发到你邮箱。模型被困在对话框里它只能基于你喂给它的信息去推理却无法主动获取信息、无法操作外部系统。1.1 问答和执行的本质区别问答是单轮信息处理输入问题输出答案。执行是多轮闭环理解目标、制定计划、调用工具、观察结果、调整策略、输出产物。这两者之间隔着一整套工程化的基础设施。拿让 agent 帮我整理周报并发送这个需求来说实际流程是agent 需要读取我的工作日志或者 git 提交记录把零散信息按周报格式组织成结构化内容调用邮件接口发送出去确认发送成功如果没有要能自己排查原因重试每一步都涉及工具调用每一步产生的结果都要反馈给模型作为下一步的参考。这就像你招了个实习生你不能只告诉他把周报发了你得给他邮箱权限、模板、收件人列表还得告诉他发完之后回来跟你汇报一句。模型就是那个聪明但缺乏经验的实习生hermes-agent 就是那个把各种资源串起来、让实习生真正干成活的流程体系。1.2 三个核心设计目标动手之前我给 hermes-agent 定了三条硬性标准后面所有设计决策都围绕这三条展开目标可拆解用户只给一个模糊的、高层级的指令agent 能把目标分解成可执行的子任务序列而不是一股脑儿想把整件事在一次调用里完成。过程可干预每一步执行都能看到中间状态能暂停、能回滚、能让人插入指令调整方向。这是 agent 进入生产环境的前提黑盒式的一次性执行只适合玩具项目。结果可验证任务结束后agent 要能基于工具执行的真实反馈来判断我到底做没做成而不是模型自己脑补一个应该成功了吧。1.3 适合做什么不适合做什么经过一段时间的实测hermes-agent 这类架构在以下场景表现最好信息收集与汇总跨多个数据源拉取信息整理成结构化报告数据处理管线清洗数据、跑分析脚本、生成可视化图表任务编排与调度定时触发、根据条件分支执行、多渠道通知内容生产辅助基于私有知识库写文档、做摘要、翻译不太适合的场景也很明确涉及资金支付、物理设备控制、法律合同签署等高风险动作至少在早期的权限设计里不应该让 agent 全自动执行。一定要做也是通过多级审批让人来兜底。这是一个要在一开始就接受的边界别等到上线了再后悔。2. 先拆骨架hermes-agent 的分层架构与模块边界很多初学者问我agent 和普通程序有什么区别。我的回答是普通程序把逻辑写死在代码里agent 把逻辑交给模型动态生成。所以 hermes-agent 的核心不是一个庞大的单体服务而是一个循环——模型在这个循环里不断思考、行动、观察、再思考。2.1 核心主循环感知-规划-行动-观察我实现的最简主循环代码如下def run_agent(user_goal: str, max_steps: int 10): hermes-agent 主循环感知 - 规划 - 行动 - 观察 context [] # 感知接收并理解用户目标 context.append({role: user, content: user_goal}) for step in range(max_steps): # 规划行动决策让模型决定做什么不直接输出答案 response llm.chat( messagescontext, toolsTOOL_REGISTRY.schema_list(), # 把可用工具告诉模型 tool_choiceauto ) message response.choices[0].message # 模型决定调用工具 if message.tool_calls: context.append(message) for tool_call in message.tool_calls: # 行动本地执行工具 result execute_tool(tool_call.function.name, json.loads(tool_call.function.arguments)) # 观察把结果回传给模型 context.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps(result, ensure_asciiFalse) }) else: # 模型认为任务已完成输出最终结果 return message.content raise TimeoutError(f超过 {max_steps} 步未完成任务)这个循环只有二三十行却是整个 hermes-agent 的骨架。它每转一圈模型就会基于当前上下文重新判断是继续调用工具还是认为任务已经完成并给出结论。关键点在于模型生成的不再是最终答案而是下一步动作。2.2 四个核心模块各自管好一摊事为了让循环跑得稳我把 hermes-agent 拆成了四个模块边界尽量清晰模块职责不负责什么大脑LLM 调用层统一管理模型接入、temperature 设置、重试策略不关心业务逻辑规划器把目标拆解为子任务序列动态调整计划不直接执行工具工具注册表维护所有可用工具的元信息和执行入口不决定工具调用顺序记忆系统管理短期上下文和长期存储不参与工具执行这个拆分的直接好处是任何一个模块出问题都能快速定位。比如工具调用结果异常直接看执行日志就行不用去猜是不是模型理解偏差如果模型频繁选错工具那就是工具描述写得不清楚跟模型本身关系不大。2.3 为什么不用现成的 agent 框架说实话我在写 hermes-agent 之前也调研对比过 LangChain、AutoGPT 这类方案。它们的思路确实有价值但实际用下来有几个绕不开的问题抽象层太厚出问题时你很难判断是模型的问题、提示词的问题、还是框架内部处理链路的问题。排查问题像是在玩找那个隐藏的环节坏了的游戏。定制不灵活很多业务场景需要精细控制比如某个工具参数必须经过转义、某个步骤需要插入人工审批。框架的通用抽象反而成了限制。依赖过重为了用框架封装好的某个功能你得引入一整套依赖而这些依赖在版本升级时很容易互相打架。从零写核心循环的成本其实并不高也许就是两三百行代码的事但对整个系统的掌控力是完全不一样的。这也是我给所有人的建议先用最简方式理解 agent 的工作原理再去决定要不要用框架、用哪个框架。3. 规划能力从哪来任务拆解与路径编排hermes-agent 的规划器本质上做的是这样一件事让模型在前一轮决策中不只是选择单个动作而是输出一份结构化的任务清单。这就把一个需要连续决策的问题拆成了先计划、再逐步执行的模式整体稳定性会高很多。3.1 从目标到子任务让模型输出 JSON 计划在 hermes-agent 里当用户给了一个较复杂的目标时规划器会先要求模型输出一份 JSON 格式的计划{ goal: 分析上月销售数据并生成报告, subtasks: [ {step: 1, action: read_file, target: sales_data.xlsx}, {step: 2, action: run_script, target: analysis.py, params: {input: sales_data.xlsx}}, {step: 3, action: generate_report, target: markdown, params: {summary: true}} ] }为什么强调 JSON 而不是让模型直接按对话方式给计划因为结构化输出可以被程序可靠地解析和调度。对话式的计划写得再漂亮代码也没法安全地基于它去做流程编排和状态跟踪。3.2 反思机制让 Agent 学会回头看只做拆解还不够更重要的一环是反思。hermes-agent 在执行完每个子任务后不会闷头冲向下一步而是会带着一个质量检查的视角去审视这一步的输出跟目标相关吗有没有偏题结果里有没有明显异常值或错误信息接下来的计划还需要调整吗我实现反思的方式也很朴素在每个工具结果回填后追加一条 system 提示要求模型简要评估当前进度与目标是否符合是否需要调整后续路径。这一步只消耗少量 token但能显著减少一条路走到黑的翻车概率。实测下来很多原本会卡死的场景——比如文件路径不存在、工具参数传错——agent 都会在反思阶段发现问题然后主动修正。3.3 一个具体案例让 hermes-agent 分析销售数据假设用户对 hermes-agent 说看下 data 目录里的销售数据按区域汇总找出增长最快的三个区域。规划器生成的计划大致是列出 data 目录找到销售数据文件读取文件结构确认字段运行数据汇总脚本按区域聚合分析环比/同比计算增长率输出 Top 3 区域及关键数字在一个没有规划器、只靠单轮 thought-action的版本里模型很有可能在第二步就试图直接调用分析工具但那时它连文件长什么样都不知道参数根本没法填对。规划的价值就是让模型先建立对全局的理解再决定每一步做什么。这也是为什么我说规划器是 hermes-agent 的战略层主循环只是战术层。4. 工具调用是 agent 的手函数注册与执行链路规划拆得再好最终都要落到干活上而干活靠的是工具。hermes-agent 的工具调用基于模型提供的 function calling 能力但把它从能调用变成稳定调用中间还有不少细致的工作。4.1 工具描述越清楚成功率越高一个工具在注册表里看起来是这样的TOOL_REGISTRY.register( nameread_csv, description读取 CSV 文件内容并返回前 N 行数据。用于数据分析、数据预览场景。文件路径必须是绝对路径或者相对项目根目录的相对路径。, parameters{ type: object, properties: { file_path: {type: string, description: CSV 文件的路径}, num_rows: {type: integer, description: 需要返回的行数默认 5} }, required: [file_path] }, funcread_csv_impl )很多人在这里犯的错是 description 写得过于随意比如只写读取 CSV 文件。模型面对多个工具时很容易因为信息不足而选错。我踩过一个典型的坑有一个发送邮件的工具和一个生成邮件草稿的工具description 都含邮件两个字结果模型经常在应该生成草稿时直接发送了邮件。这个教训让我意识到工具描述就是给模型看的使用说明书必须包含场景、边界、参数含义、注意事项。后来我在每个工具的 description 里都显式写清了什么场景用、什么场景不用、有没有副作用、是否会用外部 API选错率立刻下降了非常多。4.2 执行与结果回填闭环才能迭代工具执行完结果必须回传给模型让它基于真实结果判断下一步。这个回填环节我做了两个处理实测效果明显结果截断工具返回的数据如果很大比如读取一个几万行的 CSV直接在上下文中塞全量数据会很快耗尽上下文窗口。我会先只读前几十行做预览如果模型后续需要更多再按需分块读取。这比一次性全量加载要稳妥得多。结构包装把工具执行结果包装成{status: success, data: {...}}或{status: error, message: ...}再回填。模型看到错误信息后可以自行调整参数重试而不是面对一段看不懂的异常堆栈直接放弃。4.3 实测中最容易翻车的三个细节JSON 参数解析失败模型偶尔会输出不合法 JSON比如漏了引号、多了逗号。我的处理方法是先尝试json.loads失败后用正则把最外层花括号提取出来再解析还不行就返回一个错误信息让模型重新生成参数。这个兜底逻辑让整个调用链路的稳定性提高不少。参数类型不一致模型输出的参数全是字符串而工具定义里要求整数或列表。我在execute_tool里加了个轻量级类型转换层按 JSON Schema 的type声明对参数做强制转换转换失败会报错给模型重新生成。上下文膨胀每多一轮工具调用往返消息就会累积很快超出模型的上下文窗口。解决思路不是无限扩容而是做上下文压缩——把前面的工具执行结果用摘要替代或者让模型把当前进度状态浓缩成一段结构化文本。这个思路跟下一节记忆系统是连在一起的。5. 记忆系统上下文窗口装不下的信息放哪hermes-agent 在实际跑任务的时候上下文膨胀是我最早撞上的墙。模型有上下文长度上限而一个多步骤任务动辄产生几十条往返消息每条消息可能还带着大量工具输出。解决这个问题靠的是记忆分级。5.1 短期记忆滑动窗口加摘要压缩短期记忆指的是当前任务过程中需要持续保留的信息。最简单有效的策略是始终保留最开始的用户目标和最新的几条交互消息中间的过程细节每过几轮就让模型生成一段当前进度摘要替换掉原始消息工具执行结果里的关键数据单独提取到当前状态变量中维护这里面有个心态要摆正不是所有上下文都值得保留过去了就让它过去。很多信息当下觉得有用但等会儿模型自己会通过工具重新获取。与其把上下文填满不如想办法压缩。5.2 长期记忆向量检索加结构化知识如果 hermes-agent 要跨会话记住用户的偏好、历史任务的结果、一些领域知识那就需要长期记忆层。我用的是一个非常经典的方案把用户历史指令、任务结论、工具使用偏好转成向量存入向量数据库每次新任务开始根据当前目标的向量相似度检索最相关的历史记录作为附加上下文对于结构化知识比如用户常用文件路径、项目配置信息用 JSON 或键值对存储比向量化更直接可靠这里有一个值得提醒的坑向量检索只负责找出可能相关的记录但它不负责判断到底相不相关、相不相关到值得占用上下文。我通常会加一个轻量重排序逻辑让模型先对检索出来的候选记录做一次相关性筛选只把真正有用的放进去。没有这一步长期记忆很容易变成垃圾进、垃圾出。5.3 记忆污染问题比记不住更危险的是记住了不该记住的东西。假设用户让 hermes-agent 处理一批数据中途发现数据源有问题换了份新的如果 agent 用旧的记忆数据去分析新文件结果会错得一塌糊涂。我的应对策略是给记忆打上时间戳和失效标记。同一主题的记忆如果出现更新版本旧版本自动降权。同时每次读取长期记忆时提示模型这些是历史信息可能与当前情况不一致请以实际工具返回的实时数据为准。这句话虽然简单但能让模型养成优先相信实证而非记忆的习惯。6. 安全护栏让 agent 自主但不失控把工具权限直接交给一个受 LLM 驱动的 agent听起来有点吓人实际操作也确实需要格外谨慎。我在这件事上的原则是可以权力下放但必须设卡点、留痕迹、能熔断。就好比你让一个实习生去办事不会直接把公司公章交给他而是给一批有限的授权关键步骤还得有审批流程。6.1 工具权限分级我在 hermes-agent 的工具注册表里加了一个risk_level字段把工具分为三类风险等级示例执行策略低风险只读读取文件、搜索、数据查询自动执行无需确认中风险有限写创建草稿、生成文件、发送测试消息自动执行但结果进入审计高风险外部副作用发送邮件、删除文件、调用付费 API必须人类确认后执行中风险和高风险的判断不仅仅凭工具本身还会结合调用参数。比如生成邮件草稿算低风险但发送邮件到外部地址就是高风险模型必须停下来等用户确认。6.2 沙箱与审计日志在 party 典型部署里hermes-agent 的核心要务是两件事业务执行和过程可追踪。 我要求每个工具调用必须记录以下信息调用时间、调用链 ID、工具参数、执行结果、耗时、发起调用的模型上下文摘要。这套日志体系让很多问题都能在事后很快复盘出来。6.3 人类介入点什么时候必须停下来除了高风险工具要审批我还会设置两个强制暂停点执行步数超限如果 agent 已经执行了 8 到 10 步还没完成它会主动停下来汇报进度让用户决策是继续还是换方向。这能防止 agent 在错误的路径上越走越远。置信度不足我在工具执行错误后增加了一个重试计数同一个工具连续失败三次时agent 不再自动重试而是把问题抛给用户。这避免了模型在同一个死循环里反复打转。安全护栏加得越多agent 的自主感就会越弱。但在这个问题上我宁愿保守。生产环境的 agent 最让人放心的状态不是它什么都能自己做而是它清楚自己什么不该做、什么情况要停下来问人。7. 最小可运行实现200 行内的 hermes-agent 核心逻辑讲完设计我把我实际写的一版最简实现核心代码放出来。这个版本尽量少依赖外部包只保留主循环、工具注册、规划器三个关键部分让你能在一个下午跑通整个闭环。7.1 环境与依赖我用的是 Python 3.10以及一个支持 function calling 的大模型 API以 OpenAI 兼容接口为例也可以用本地部署的模型。依赖只需要两个openai和python-dotenv另外用标准库做 JSON 处理。pip install openai python-dotenv7.2 核心代码实现先定义一个最小的工具注册表和两个示例工具import json import openai TOOLS [] def register_tool(name, description, parameters, func): TOOLS.append({ type: function, function: { name: name, description: description, parameters: parameters } }) globals()[f_impl_{name}] func # 简化演示 def execute_tool(name: str, arguments: dict): impl globals().get(f_impl_{name}) if not impl: return {status: error, message: f未知工具: {name}} try: result impl(**arguments) return {status: success, data: result} except Exception as e: return {status: error, message: str(e)}注册两个常用工具一个是读取文件一个是执行 Python 表达式做计算def read_file_impl(file_path: str, num_chars: int 2000): with open(file_path, r, encodingutf-8) as f: return f.read()[:num_chars] def calculate_impl(expression: str): # 注意这里仅用于可信表达式的简单演示生产环境绝不能直接用 eval return eval(expression) register_tool( nameread_file, description读取文本文件的内容返回前 num_chars 个字符。适合读取配置文件、文档、数据文件。, parameters{ type: object, properties: { file_path: {type: string, description: 文件路径}, num_chars: {type: integer, description: 最多返回字符数默认2000} }, required: [file_path] }, funcread_file_impl ) register_tool( namecalculate, description计算一个数学表达式例如 1 2 * 3。用于数值计算和数据验证。, parameters{ type: object, properties: { expression: {type: string, description: 数学表达式} }, required: [expression] }, funccalculate_impl )然后是主循环。为了让逻辑更贴近任务执行而不是闲聊我要求模型在有工具可用时必须先调用工具除非明确判断任务已完成client openai.OpenAI() def run_hermes_agent(user_goal: str, max_steps: int 6): messages [ {role: system, content: 你是 hermes-agent一个能调用工具完成任务自主智能体。 对复杂任务请先在思考中列出计划然后逐步调用工具执行。 完成任务后用中文简洁地总结结果。}, {role: user, content: user_goal} ] for step in range(max_steps): response client.chat.completions.create( modelMODEL_NAME, messagesmessages, toolsTOOLS, tool_choiceauto, temperature0.2 ) message response.choices[0].message if message.tool_calls: messages.append(message) for tc in message.tool_calls: args json.loads(tc.function.arguments) result execute_tool(tc.function.name, args) print(f[step {step}] 调用 {tc.function.name}({args}) {result}) messages.append({ role: tool, tool_call_id: tc.id, content: json.dumps(result, ensure_asciiFalse) }) else: return message.content return 达到最大步数任务未完成。最后试一个任务让 agent 自己先看代码目录里的说明文件再算一个数字if __name__ __main__: result run_hermes_agent(读取项目里的 README.md找到其中提到的示例数值然后算出该数值乘以 3.14 的结果。) print(最终结果:, result)7.3 跑起来的实测观察这套最小实现跑通之后我有几个很直观的感受工具描述质量直接决定成败当read_file的描述里明确写了适合读取配置文件、文档、数据文件模型就会干净利落地选它描述模糊时模型会犹豫甚至输出一个不存在的工具名。JSON 解析偶尔还是会挂虽然我加了json.loads但面对模型输出的一段带前后缀说明的 JSON还是会解析失败。实际版本里我加了一段提取逻辑把第一个{到最后一个}之间的内容先截出来再解析这个辅助逻辑很管用。任务完成信号没想象中可靠在没有额外提示的情况下模型有时候会中间就给出总结而不是继续调用工具比如它读了一部分文件就开始下结论。我通过 system 提示里强调先调用工具获取全部信息再输出最终结果并在主循环里对提前总结做了拦截这个问题才得到缓解。7.4 后续可以往哪些方向扩展这个最小实现是骨架后面能挂的东西很多。我自己的 hermes-agent 大致扩展方向是多工具矩阵接上数据库查询、HTTP 请求、邮件发送、内部 API 等工具覆盖真实业务场景规划器改造从每轮直接决策升级为先规划再执行的两阶段结构复杂任务稳定性会提升一截持久化记忆把历史任务结论写入向量库让 agent 在新任务里能引用旧经验多 Agent 协作拆成规划 Agent和执行 Agent规划 Agent 负责拆解和验收执行 Agent 专门与工具打交道两个角色各用不同的 prompt 和上下文从一个想法到一版能跑的 hermes-agent核心代码不过 200 行上下背后真正值钱的不是代码本身而是对模型如何与外部世界交互这一整条链路的设计理解与踩坑验证。做一个能跑通的 demo 并不难真正的挑战在于把稳定性一步步打磨到能用于生产环境。如果你刚开始做自己的 agent 项目我的建议很直接别一上来就堆一堆框架先用最朴素的方式把主循环、工具调用和记忆这三件事跑明白再带着问题去扩展。那样你踩进坑里的时候至少知道自己是怎么掉进去的。
返回列表