ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

从零到一设计一个AI Agent框架:hermes-agent实战解析

从零到一设计一个AI Agent框架:hermes-agent实战解析 从零到一构思一个AI Agent框架我为什么做了hermes-agent大约半年前我在折腾一个自动化工作流的项目反复用各种现成的Agent框架写Prompt、配工具、调记忆折腾了很久总感觉差那么一点意思。不是功能不够强而是太重了——有的框架光学习成本就够我喝一壶有的工具绑定得死死的想换个模型都要改半天。后来我索性自己动手搭了一个轻量级的Agent框架名字就叫hermes-agent。Hermes是希腊神话里传递讯息的信使速度敏捷、沟通畅通正好拿来形容一个在现代大模型应用里负责“跑腿干活、传话调工具”的AI代理角色。这篇文章想写的东西不是一份正经八百的框架技术文档而是把整个思考过程和实操细节掰开揉碎地讲一遍我是怎么定义问题域的、为什么选择某些方案、各个核心模块是怎么落的、踩了哪些坑又怎么填的。如果你也正打算做一个Agent类应用或者只是好奇这一类项目内部大概长什么样希望这篇内容能给你一种“同行之间聊项目”的参考价值。1. 需求梳理做Agent先从“它是谁”开始1.1 想清楚Agent到底解决什么问题很多人在做Agent框架时上来就开始写代码先对接大模型API再搞个工具执行器再套一层记忆存储……但往往做到一半就发现产品的形态特别模糊。因为“Agent”这个帽子太大了它可以指一个能对话的聊天机器人也可以是自动化测试里自主操作的测试代理还能是帮运营发日报的定时机器人。我在动手之前先问了自己三个问题这个Agent的核心任务是什么是回答问题、执行操作还是两者兼顾如果是执行操作那么它需要依赖哪些外部系统使用者是谁是普通用户、开发者还是企业内部员工这三个问题直接决定了框架的边界。我当时的目标非常清楚做一个以“任务编排”为核心的轻量级Agent框架支持多模型接入、工具扩展、任务状态跟踪并且对外提供HTTP接口方便业务方集成。它不是面向C端的聊天产品而是给B端团队做业务自动化用的“代理层”。想明白这一点后面的设计就顺了。这里给立项阶段的同伴们一句忠告先给项目划定边界然后立一个“最小可用”的版本目标。hermes-agent第一个版本我只要求三件事——能跟大模型对话、能用注册好的工具完成一次实际调用、能在任务失败时拿到清晰的错误反馈。超出这个范围的统统往后放。1.2 hermes-agent的定位与核心能力结合这个目标我把hermes-agent的定位最终确定为一个让开发者“五分钟接入、半小时定制”的Agent运行框架。它的核心能力大致可以拆成几个层面模型接入层底层模型可替换通过统一接口对接不同大模型服务或本地模型。工具系统工具即函数开发者只要按约定写一个普通函数注册后Agent就能在适当场景自动调用。任务调度Agent可以把复杂任务拆解成多步循环观察、思考、行动、观察结果再决定下一步。状态与异步支持任务可能耗时长不能一个接口傻等到底需要支持异步提交和回调通知。权限和安全边界工具调用不能毫无约束需要做一些基础的权限控制与会话隔离。这个框架本质上干的事情是充当“大模型与外部世界之间的路由器”。大模型本身没法查询数据库、没法发请求、没法操作文件Agent框架的意义就在于把这些外部能力包装成一组工具然后让模型在完成任务的过程中按需选择和使用这些工具。所以hermes-agent最核心的设计原则是“约束下的自由”。所谓自由是给模型足够的决策空间去规划步骤所谓约束是每一步工具调用的边界、参数格式、可用范围都是开发者事先定义好的。2. 技术方案选型为什么这些组件凑在一起2.1 架构层面的取舍单体还是微服务做框架和做业务系统不一样框架本身为了易嵌入和易扩展当初我基本没有犹豫就选了“单体核心插件式扩展”的架构风格。原因很简单大部分业务团队没有精力维护一套分布式依赖单体框架可以让使用者在自己的进程内直接调用减少网络开销。Agent框架的生命周期管理相对集中单体模式更容易控制状态变更和上下文传递。插件式的工具注册机制让单体框架照样能支持高扩展性。这里插一句背景市面上很多Agent项目上来就用消息队列、独立数据库、流式处理引擎搞得像高并发微服务集群。但Agent的瓶颈往往不在吞吐量而在模型推理的链路延迟和工具集合的编排灵活度。用最简单的进程模型承载核心逻辑反而更容易把精力花在刀刃上。2.2 语言与生态选定Python的核心理由语言选型上我在Node.js和Python之间摇摆过一段时间。Node.js胜在事件驱动异步性能好做实时交互很顺手但考虑到当前大模型生态对Python最友好无论是LangChain这类现成组件、各种模型SDK还是数据处理库基本都是Python优先为了让hermes-agent后续能给更多人快速用起来最终还是选了Python。Python在Agent框架领域还有一个隐藏优势动态特性非常适合做工具反射注册。开发者定义函数框架通过类型注解和文档字符串自动生成给模型看的工具描述这个过程在静态语言里要写很多样板代码在Python里几行就能搞定。当然Python也有它的问题比如性能天花板低、类型约束弱。但在Agent这个场景下真正的性能瓶颈不在语言本身而在大模型API的往返延迟。一次工具调用链路可能模型推理耗时几秒甚至几十秒Python在这里多花几毫秒完全无感。2.3 消息协议与对外接口为什么优先HTTP和外部系统交互的接口第一版我只提供了基于HTTP的同步/异步接口之后才考虑加WebSocket做实时推送。这样做的最直接原因是HTTP的通用性最好任何语言、任何平台都能方便调用业务方集成成本极低。在接口设计上我将所有请求数据封装为JSON并定义了统一的请求和响应结构。比如一个提交任务的请求通常会包含这几个核心字段字段类型含义task_idstring业务方生成的任务唯一标识session_idstring会话标识用于区分不同对话上下文user_inputstring用户输入的指令内容toolsarray本次任务允许使用的工具名称列表configobject模型参数、超时时间等运行配置响应里除了最终结果还会带上完整的执行轨迹包含每一步的思考内容、调用工具、工具返回结果和耗时。这对调试和复盘特别有用。实际接业务的时候技术负责人最关心的往往不是“你的Agent多聪明”而是“出了问题我能不能快速定位”所以执行轨迹从第一天我就设计成一级公民。3. 核心模块拆解与实现细节3.1 模型接入层的统一抽象模块设计的第一件事是抽象模型接入层。现在大模型厂商很多各家API风格千差万别如果框架里写死某个厂商的调用将来想换模型就是灾难。我在这一层定义了一个统一的model provider接口核心方法就两个chat(messages, config)传入消息列表和配置返回模型输出和消耗统计。stream_chat(messages, config)流式接口适合打字机效果和长回复场景。再往下一层每个具体的模型服务商就是一个实现类负责把统一的请求参数翻译成该服务商要求的格式再把返回结果解析成框架统一的数据结构。这个抽象的价值在什么时候体现最明显就是业务方跟你说“我想把模型从A换成B测试一下哪个效果更好”。你要是直接写死了厂商SDK这种需求怎么也得改业务代码有了这层抽象改一个配置项或者注册一个新的Provider实例十分钟完事。配置上我建议把这些内容统一放到环境变量或配置文件里例如# config yaml 示例 model_provider: name: openai api_key_env: MODEL_API_KEY base_url: https://api.example.com/v1 default_model: gpt-4o-mini特别注意API Key千万别写死在代码里也别提交到Git仓库。实际项目里因为误提交API Key导致泄露的新闻每年都有Agent类应用因为能调用工具密钥泄露造成的风险比普通应用更大。3.2 工具注册机制函数即工具工具系统是Agent框架的灵魂。设计工具注册机制时目标只有一个让开发者写一个普通Python函数就能立刻变成一个“模型可以调用的工具”。具体实现起来分两层逻辑。第一层是注册表。框架内置一个全局工具注册表本质是一个dictkey是工具名value是工具对象。工具对象里除了回调函数本身还保存了name、description、参数schema这些元信息。第二层是格式转换。模型调工具时需要按API规范传入结构化参数。框架要做的事情是读取函数的签名和类型注解结合函数docstring自动生成一份模型能理解的JSON Schema描述。有了这个描述模型就知道“什么时候该调这个工具参数该怎么填”。一个最简工具的实现长这样from hermes_agent import ToolRegistry registry ToolRegistry() registry.register def get_weather(city: str, date: str today) - str: 查询指定城市在指定日期的天气情况。 Args: city: 城市名比如北京、上海。 date: 日期格式YYYY-MM-DD默认为今天。 Returns: 天气摘要信息。 # 这里写真实查询逻辑可以调外部天气API return f{city} {date} 晴25°C注册完成后模型在需要天气信息时会看到工具列表里多了一项get_weather并知道它需要两个参数city和date。框架收到模型发出的调用请求后会做参数校验执行函数把返回值重新塞回对话上下文让模型基于新信息继续思考。这一个来回就是Agent区别于ChatBot的关键能力。做工具系统有几点细节提醒一下函数名就是工具名时命名风格要统一建议全小写加下划线避免特殊字符。docstring质量直接影响模型识别工具的准确度。写清楚用途、参数含义、返回值含义效果天差地别。不是所有函数都应该注册给模型。只暴露必要能力减少模型误调的概率。3.3 任务编排循环从“思考到行动”的闭环Agent的核心运行机制是一个循环接收用户输入 - 模型思考生成回复或工具调用请求 - 执行工具如果有 - 把工具结果反馈给模型 - 模型决定是继续调工具还是给出最终答案。在hermes-agent里我用一个AgentLoop类管理这个循环。循环上限默认设置在10轮左右防止模型陷入反复调工具的“死循环”。伪代码逻辑大概是while round_count max_rounds: # 1. 组装消息加上系统提示词、历史记录、工具描述 messages build_messages(session, tools) # 2. 调用模型拿到回复 response provider.chat(messages, config) # 3. 判断模型是否想调工具 tool_call response.tool_calls if not tool_call: # 没有工具调用说明模型给出了最终回答直接返回 return response.content # 4. 执行工具把结果作为一条新消息加入会话 tool_result execute_tool(tool_call) scene.add_tool_result(tool_result) round_count 1这里有一个很重要的工程设计细节工具返回结果怎么塞进上下文。多数模型API要求消息要么来自user要么来自assistant还有专门的tool消息类型。我在抽象层里做了归一化处理把工具调用结果包装成特定格式的消息对象保证不同模型的兼容性。3.4 上下文管理与会话隔离Agent应用里最容易被低估的部分其实是上下文管理。每次循环都要把会话历史发给模型如果历史无限增长早晚会超出模型的上下文窗口而且成本也随token量线性上涨。我采用的方案是“滑动窗口摘要压缩”的组合策略近几轮对话完整保留确保模型有足够细节进行推理。超过一定长度的历史记录调用一次模型做摘要压缩保留关键信息点。工具返回的超长结果比如查询了几百行数据自动截断到指定长度并把截断情况标注清楚告诉模型“结果不完整如有需要可缩小查询范围再试”。会话隔离方面我为每个会话维护独立的上下文集。用户A的对话无论如何不会被塞进用户B的上下文里这个在业务落地时是硬性要求防止数据串线。另外提一个容易被忽略的小点每个任务的元数据比如执行时间、token消耗、模型名称也要跟着会话走这样后面调日志、算成本、做分析都有据可查。3.5 权限控制与安全边界Agent能调工具意味着它有“手”。有手是好事但也有风险。如果模型被诱导调用了删除接口或读出了不该读的数据后果不堪设想。我给hermes-agent加的安全机制有好几层工具白名单每个任务启动时明确指定allow_tools列表未在列表里的工具模型看不到也调不了。参数校验模型传入的参数必须符合Schema定义类型错误的直接拒绝执行。人工审批钩子对高危操作支持配置人工确认环节。业务方可以在工具执行前插入审批回调只有审批通过工具才真正执行。操作审计日志所有工具调用记录持久化方便事后审计追溯。这一块我态度很明确作为框架宁可多一层检查慢一点也不要不设防地让模型横冲直撞。Agent类应用上线前权限自查必须当成必做项而不是可选项。4. 实操记录从环境搭建到一次完整调用4.1 环境准备与模块初始化纸上谈兵不够我们走一遍真实流程。首先安装依赖我用的是uv做包管理比传统的pip快不少但为了兼容性下面用常规pip方式演示。pip install hermes-agent装好之后初始化一个项目目录结构大体如下my_agent_project/ ├── main.py ├── tools/ │ ├── __init__.py │ ├── weather_tool.py │ └── calendar_tool.py ├── config.yaml └── .env.env文件里放环境变量内容类似MODEL_API_KEYsk-xxxx LOG_LEVELINFOmain.py里做初始化加载配置、注册工具、启动服务from hermes_agent import HermesAgent, load_config config load_config(config.yaml) agent HermesAgent(config) # 加载业务自定义工具包 from tools import weather_tool, calendar_tool agent.register_tool(weather_tool) agent.register_tool(calendar_tool) # 启动HTTP服务 agent.serve(host0.0.0.0, port8080)这里有个细节工具模块要在注册前完成导入不然工具列表是空的。这个顺序问题我踩过几次坑后来干脆在框架启动时加了“已注册工具列表”的空检查防止配置半天结果模型一个工具都调用不了。4.2 编写第一个业务工具以“会议室预约”为例这个工具很能展示Agent的实际价值。registry.register def book_meeting_room(room_id: str, start_time: str, end_time: str, topic: str) - str: 预约会议室。 Args: room_id: 会议室编号比如B-302。 start_time: 开始时间ISO格式。 end_time: 结束时间ISO格式。 topic: 会议主题。 Returns: 预约成功返回确认号失败返回错误信息。 # 业务侧实现对接内部会议室系统API resp booking_client.book( room_idroom_id, startstart_time, endend_time, topictopic ) return f预约成功确认号{resp.confirmation_code}这段代码体现的就是“函数即工具”开发模式。业务开发人员根本不用关心AI怎么工作只要用正常写函数的思维把能力暴露出去Agent自动会学会在什么场景下把对应能力用起来。4.3 发起任务请求与结果解析服务启动后外部系统通过HTTP接口提交任务。curl -X POST http://localhost:8080/v1/task \ -H Content-Type: application/json \ -d { session_id: sess-123, user_input: 帮我预约明天下午2点到3点的B-302会议室主题是季度复盘, allowed_tools: [book_meeting_room] }返回结果示例{ task_id: task-0001, status: completed, output: 已为您成功预约B-302会议室时间明天14:00-15:00主题季度复盘。确认号C-8871。, trace: [ { step: 1, type: tool_call, tool: book_meeting_room, input: { room_id: B-302, start_time: 2025-06-11T14:00:00, end_time: 2025-06-11T15:00:00, topic: 季度复盘 }, result: 预约成功确认号C-8871, elapsed_ms: 312 } ], usage: { prompt_tokens: 1240, completion_tokens: 96, total_tokens: 1336 } }注意看响应里的trace字段。它记录了模型每一步的实际决策过程。最终输出是模型生成的面向用户的自然语言但执行轨迹是给开发者和审计人员看的。有了这个数据线上出问题时能直接指出是哪一步不对劲而不是对着黑盒发愁。4.4 流式输出与异步任务处理上面是同步场景。有些任务耗时长比如“对比最近一个月销售数据生成分析报告”可能需要好几轮工具调用时间跨度很大。对这种任务我提供了异步模式。客户端提交任务后接口立刻返回{ task_id: task-0009, status: running, message: 任务已受理请通过轮询或回调获取结果 }任务完成时如果配置了webhook回调URL框架会主动POST结果到这个地址。如果没配置调用方可以用task_id轮询查询状态。流式输出方面我用的是WebSocket 事件流。每次工具调用结束或者模型输出了一部分新话语都会推送一个事件给前端。这样前端页面就能像“直播”一样展示Agent的执行过程用户体验和调试体验都会好很多。我后来复盘时认为做异步这步非常正确Agent任务的不确定性决定了同步等待会拖垮整个业务的响应时延异步才能真正贴近生产环境。5. 常见问题与避坑实录5.1 模型总是重复调用同一个工具怎么办这是我在项目里遇到频率最高的问题典型的“Agent死循环”症状。模型不断调用某个工具拿到结果后又调一次像卡住了一样。排查下来原因主要有三类工具返回信息不足以支撑模型做出下一步判断模型只好再调一遍碰运气。解决思路是先想办法把工具返回结果做结构化、尽量让信息量充足。上下文里缺少“工具结果已使用”的标记模型困惑于该行动还是该总结。解决思路是调整提示词明确要求模型在拿到工具结果后如果已经得到答案就直接给最终结论。循环上限设置得不合理或者提示词里完全没提“最多行动几次”。我在系统提示词里会固定加一句如果连续多次工具调用无法推进任务请暂停并说明困难和已完成的部分。5.2 模型把工具参数理解错了比如工具需要的是ISO格式时间模型传入“明天下午2点”。这类问题的根源在于参数描述不够清楚模型只能靠猜。解决办法是改工具的description把格式要求写得更具体。registry.register def book_meeting_room(room_id: str, start_time: str, end_time: str) - str: 预约会议室。 Args: room_id: 会议室编号比如B-302。 start_time: 开始时间ISO 8601格式例如2025-06-11T14:00:00。 end_time: 结束时间ISO 8601格式例如2025-06-11T15:00:00。 把格式示例直接写死在docstring里等于给模型塞了一张“填空模板”效果立竿见影。这里面还有个细节参数名本身也要有自解释性。用start_time而不是start或time1模型理解起来就容易得多。5.3 上下文越塞越满还没到问题答案就炸了超长历史导致token超限是另一个高频问题。前文提到的“滑动窗口摘要”策略就是为了缓解这个。实操中我还发现一个更见效的小技巧工具结果里的长列表不要整个塞进上下文而是先做预处理——比如只返回统计信息和前几条示例记录必要时再提供分页查询的工具让模型自己按需获取更多数据。这样做的额外好处是减少token消耗省成本。Agent跑起来后成本主要烧在token上上下文优化做得好一个月省下的API费用相当可观。5.4 框架运行稳定性相关的问题Agent框架跑在生产环境稳定性是底线。除了依赖系统本身要稳我另外总结几条经验给每个外部依赖调用加超时。特别是模型API网络抖动一次可能卡住整个任务。超时时间建议模型推理之前按响应大小动态调整。工具执行要加独立的异常捕获。某个工具挂了不应该导致整个任务进程崩溃而是要把错误信息反馈给模型让它决定如何降级处理。并发任务场景下注意会话状态的线程安全。我用的是按session加锁的策略同一个session的任务串行执行不同session之间并行兼顾效率和一致性。所有任务的执行轨迹、token消耗、耗时落地成结构化日志。排查问题时这些日志是唯一的可靠依据。5.5 问题排查速查整理一个快速对照表按“症状-原因-对策”梳理症状可能原因排查方向模型不调用工具工具描述不清晰、工具未注册成功检查工具列表输出对照docstring工具反复被调用上下文缺少总结引导、返回信息不充分增强工具返回结果优化提示词参数格式错误工具描述缺少格式示例在docstring中补充参数样例任务执行超时模型API慢、单轮工具链路过长调大超时限制优化循环轮次上限上下文超过限制历史累加过长、工具返回过大启用摘要压缩截断长结果token消耗过高工具结果冗长、系统提示词过重精简提示词缩小工具返回体量6. 从框架到产品还缺哪些东西6.1 可观测性是Agent应用的命门自己用和做成产品给人用最大的区别在于可观测性。Agent框架是个典型的高不确定性系统模型可能出现五花八门的行为。没有一套成熟的可观测体系线上出了问题基本只能靠猜。hermes-agent目前提供的trace日志算是基础版可观测。往深了做我觉得至少还需要三块指标监控任务成功率、平均耗时、token消耗趋势、工具调用分布。链路追踪一次任务从一个入口出发经历了哪些模型调用、工具调用每一步耗时多少、结果如何完整还原。样本回放把线上典型任务“录下来”后续模型或工具变更后回放对比看行为是否异常。这些组件单个看都简单但组合起来就是Agent应用的生产力保障。6.2 内置人机协同能力Agent目前还不能完全做到无人值守。特别是在企业环境里很多高风险操作需要人工确认。我理解的下一代Agent框架应该把“人机协同”当作一等公民来设计任务执行到关键节点时可以暂停挂起等待人工审批或补充信息。人工可以中途接管某个任务修改模型的执行计划然后继续交给Agent跑。每次人工介入的记录都可以作为反馈样本存入审计改进后续行为。这个方向不仅影响产品体验还直接影响用户对Agent的信任度。信任度建立不起来Agent做得再强也难以落地。6.3 插件生态与社区化一个框架的价值很多时候看生态。hermes-agent如果只靠开发者自己写工具天花板很低。理想状态是有一个插件市场开发者贡献会议室预订、邮箱处理、工单系统、数据查询等现成工具使用者装了就能用。这就是为什么一开始工具系统设计成“注册制元数据描述”就是为了将来插件化时足够通用。后面我也在考虑做一个官方的工具仓库把常见系统集成写成开箱即用的插件包让业务方一行命令搞定安装。这件事工程量不小但方向是明确的。7. 一点心得体会做了这么久hermes-agent我最大的感受是做Agent框架技术细节当然重要但更关键的是要有清晰的“产品思维”。你以为你在写代码实际是在设计一套人机协作的协议——明确模型的自主度在哪里明确工具的权限边界在哪里明确人的干预点在哪里。这三件事想不清楚再好的模型、再多的工具也是脆弱地黏合在一起。我在实际使用中发现一个靠谱的最小闭环比一个“看起来什么都能做”的半成品有价值得多。如果你也想做类似项目我建议从一个小场景切入比如“自动从邮箱里提取订单信息写入表格”先把这个场景跑通、跑稳再考虑扩展开去。这条路看起来慢实际上是最快能看到成果的方式。我也会在后续版本里继续完善会话记忆、插件市场这些模块毕竟框架这东西是养出来的不是一个版本就能封神的。
返回列表