轻量级Agent框架设计:核心架构、Function Calling与工程落地实践

发布时间:2026/9/9 4:35:22
轻量级Agent框架设计:核心架构、Function Calling与工程落地实践 前言做了一年多大模型应用我最大的感触是调 API 谁都会但把一个 AI 能力真正落进业务流里难的是那层“胶水”。模型输出要解析、工具要按需调用、状态要维护、异常要处理这些事零零散散每次都要重写一遍。后来我实在不想再重复造轮子了就把自己常用的那套东西抽出来做成了一个叫hermes-agent的轻量级 Agent 框架。项目名取自希腊神话里的信使神赫尔墨斯——在我看来Agent 的本质就是一个“信使”它把用户的意图翻译成模型能理解的指令再把模型产生的决策转成工具调用最后把结果送回给用户。这个定位很适合我自己做内部工具集合的场景在 GitHub 上开源后也收到了一些反馈。这篇文章就把整个项目的设计思路、关键实现和踩过的坑从头到尾捋一遍给同样在折腾 Agent 的人一个参考。1. 项目整体设计思路写 hermes-agent 之前我试过 LangChain、AutoGPT 这类成熟框架。它们功能确实全但问题也明显抽象层级太多调试一个普通的数据查询任务要翻好几层封装依赖又重很多功能我用不上。后来我决定自己写一个只保留我认为最核心的东西。1.1 Agent 的“信使”核心定位整个项目的核心定位很简单接收用户的自然语言请求理解并拆解成可执行的子任务调用外部工具或内部服务完成子任务汇总结果并返回给用户你会发现这个流程里没有“独立思考”的空间Agent 不是一个自动写代码的机器人而是把大模型的语义理解能力和外部工具的执行能力串起来。这样设计的好处是边界清晰。我不需要 Agent 去“创造”只需要它准确、可靠、可追踪。尤其是接内部系统的时候一个动作是由哪条工具触发的、传了什么参数、返回了什么结果必须能审计。所以 hermes-agent 从一开始就围绕可观测性做设计而不是一味追求能力上限。1.2 为什么叫 Hermes而不是叫 AI-Copilot 之类名字这事看着小其实反映了我对这个项目的心态。“Copilot”“Assistant”这类名字暗示的是一个和你并排坐的伙伴而“Hermes”暗示的是传递消息的信使。后者的定位更谦逊也更准确——Agent 的输出最终是要交给下游系统去执行的它必须忠实传达并验证每一步结果。在实际使用中这个定位给我省了很多事。以前用某种通用 agent 做数据库查询时模型偶尔会自作主张地改查询条件用户问东它答西。在 hermes-agent 里我加了一条硬约束工具的入参必须由工具自己声明的 schema 校验模型无权静默修改参数。你等下看实现部分会明白这条规则是怎么落到代码里的。1.3 项目能做什么、适合谁用一句话总结hermes-agent 是一个面向内部服务集成场景的轻量级 Agent 运行时。它能做这些事把自然语言问题转成结构化的工具调用支持多轮对话和上下文记忆可插拔工具注册机制新增一个接口只需 10 分钟流式返回适合聊天气泡式的交互完整的日志链路每个工具调用都可追溯适合的人群不想用重型框架、又有一定 Python 基础想在公司内部快速搭一个“问答式自动化助手”的开发者。坦白讲它的生态和社区没法跟 LangChain 比但如果你只要核心的 tool-calling 能力它的代码量少、易读改起来很顺手。2. 核心架构拆解这一部分我把自己最看重的几个设计点展开讲包括 Agent 主循环、工具注册机制和记忆管理。这些是 hermes-agent 的骨架理解了它们后面看代码就顺畅了。2.1 Agent 主循环感知-决策-执行hermes-agent 的核心是一个 while 循环叫做 Agent Loop。它的逻辑非常朴素每次迭代做三件事感知把当前对话历史、用户最新输入、可用工具列表拼接成 prompt发给大模型决策模型返回两种结果之一——要么是最终回答文本要么是一个工具调用请求函数名参数执行如果是工具调用就校验参数、执行工具、把结果追加到对话历史里然后进入下一轮循环这个循环什么时候结束两种情况下结束模型返回了最终回答或者达到了最大迭代次数默认 10 次。你可能觉得 10 次有点多实际上我测下来绝大多数任务在 3 次以内就能完成设置上限只是为了防死循环。# hermes-agent 核心循环的简化版本 async def run_agent(user_input: str, max_iterations: int 10): messages build_initial_messages(user_input) for step in range(max_iterations): response await llm_client.chat( messagesmessages, toolsget_tool_schemas(), # 把所有工具描述传给模型 ) if response.tool_calls: # 模型想调用工具我们代为执行 for tool_call in response.tool_calls: result await execute_tool(tool_call) messages.append(tool_result_message(tool_call, result)) else: # 模型给出最终回答返回给用户 return response.content raise AgentTimeoutError(超过最大迭代次数)这段代码看着简单但有几个细节值得展开。第一为什么要把所有工具的 schema 都给模型因为大模型本身没有“选择工具”的能力它是根据你提供的函数描述来决定调不调用的。所以工具的描述信息写得清不清楚直接决定调用准确率。我在实践里总结了一条经验工具描述里一定要写清“什么场景该用、什么场景不该用”负向提示比正向提示更能减少误调用。比如一个查询天气的工具描述里写“当用户询问当前天气、温度、降水概率时使用闲聊天气感受类话题不要使用”误触发率会明显下降。第二执行工具时要不要做并发如果模型一次返回了多个 tool_call理论上应该并行执行。我在最开始是串行的后来发现模型经常在一个任务里同时查用户信息和查订单列表串行明显拖慢响应就改成了用 asyncio.gather 并发执行。改完之后同样任务的耗时就从一个一个排队变成了“最慢那个工具”的时间体感效果好不少。但要注意并行执行时如果两个工具之间有依赖关系比如先用 A 拿 token再用 B 查数据必须分成两轮。2.2 工具注册表十行代码接一个新接口工具注册表是 hermes-agent 里我花心思最多的模块。它解决的核心问题是怎么让“新增一个工具”这件事变得足够简单简单到团队里任何人都不需要理解 Agent 的原理。实现上用了一个装饰器模式。你只需要写一个普通的 Python 函数加上 tool 装饰器声明名称和描述参数用 type hint 标注类型系统会自动帮你做参数校验和 schema 生成。from hermes_agent import tool tool( namequery_order, description按订单号查询订单详情。当用户提到订单状态、物流、金额时使用。, ) def query_order(order_id: str, include_items: bool False): 调用内部订单服务查询订单信息 url fhttp://internal-order-service/api/orders/{order_id} params {include_items: include_items} # 这里只是示意实际会走 HTTP 调用并做超时处理 return http_get_with_retry(url, params)有了这个注册表底层的事情它是这样处理的扫描被 tool 装饰的函数提取函数名、docstring、参数类型自动转成 OpenAI 风格的 JSON Schema在 Agent 初始化时注册进 tools 列表随每次请求传给模型模型发起调用时先用 Schema 校验入参是否合规不合规直接拦截避免把脏参数传进内部系统这里有一个很多人容易忽略的点函数的 docstring 就是模型“看懂”这个工具的唯一窗口。你写参数注释写得再明白模型看不到模型只能看到 JSON Schema 里的 description 字段。所以我在代码里把 docstring 的解析做得很细第一行作为工具简介后面几行如果写了“注意事项”会被单独解析出来放进 schema 的 description 里。这个设计帮我解决了很多“模型乱调用工具”的问题。2.3 记忆与上下文管理多轮对话的 Agent 一定会遇到上下文爆炸的问题。hermes-agent 的默认策略是保留最近 N 轮完整对话更早的内容做一次摘要。def build_context(messages, max_recent_rounds10): recent messages[-max_recent_rounds * 2:] # 每轮包含 user/assistant 两条 earlier messages[:-max_recent_rounds * 2] if earlier: summary summarize_conversation(earlier) # 调用 LLM 生成一段摘要 return [{role: system, content: f更早的对话概要{summary}}] recent return recent摘要本身也会消耗一次模型调用所以我对频率做了控制只有早期消息超过 20 条时才触发摘要。还有一个细节是工具调用结果也要进上下文。否则模型第一轮查完订单号第二轮就忘了还得重新查。工具执行结果会以 system 消息的形式放回上下文里并且在旁边标注对应工具名方便模型理解这段内容是谁产生的。3. 关键实现细节架构定了之后真正写起来还是有不少“坑”。这一节我挑三个最关键的实现点详细讲模型层抽象、Function Calling 的解析、以及流式输出。3.1 模型接入层抽象市面上各家模型都有自己的一套调用格式OpenAI 的 chat.completions、Claude 的 messages、国内各家厂商的也有差异。hermes-agent 在模型层做了抽象定义了一个统一的 LLMClient 协议class LLMClient(Protocol): async def chat( self, messages: list[dict], tools: list[dict] | None None, ) - LLMResponse: ... async def chat_stream( self, messages: list[dict], tools: list[dict] | None None, ) - AsyncIterator[LLMStreamEvent]: ...这么设计的好处有两个。一是测试方便我写了一个 MockClient 在测试里使用不需要真调 API 就能验证 Agent 主循环的逻辑。二是换模型成本低之前内部有个场景必须用一个特定厂商的模型我只需要写一个适配器二十几行代码就能接进来不用动 Agent 核心逻辑。很多人会纠结“我应该接入哪个模型”我的建议是先用你最顺手、生态最成熟的那个把链路跑通以后要换再抽象。不要一开始就为抽象而抽象那才是真正的过度设计。3.2 Function Calling 解析与执行Function Calling 是 Agent 的核心能力它的完整链路是模型返回一个结构体里面包含 tool_calls 数组每个 tool_call 里有 function.name 和 function.argumentsJSON 字符串系统根据 name 找到注册表里的对应函数解析 arguments JSON并按 schema 校验参数类型执行函数把返回值序列化后作为消息追加回上下文如果执行异常把错误信息也追加回去让模型自己决定下一步这里最值得注意的就是参数校验。我遇到过很多次模型生成了非法 JSON 的情况比如参数值里带了个多余逗号、字符串引号没转义。hermes-agent 的处理方式是先尝试 json.loads 解析失败后用正则做一次轻量修复补括号、去尾逗号再不行就返回一个格式化的错误信息给模型重试。def parse_tool_arguments(raw_args: str) - dict: try: return json.loads(raw_args) except json.JSONDecodeError: # 常见修复去掉尾随逗号 fixed re.sub(r,\s*([}\]]), r\1, raw_args) try: return json.loads(fixed) except json.JSONDecodeError as e: raise ToolArgumentError(f参数解析失败: {e}) from e这个修复逻辑可能看起来很简单但在实际使用中救了我很多次。有的模型在生成复杂嵌套 JSON 参数时特别容易在尾部多加一个逗号去掉之后往往就能解析成功。如果还失败就把错误原样返回给模型模型基于错误信息重新生成那轮参数成功率也还行。3.3 流式输出与状态管理如果 Agent 只做工具调用用户那边可能几秒钟没反馈体验很差。hermes-agent 支持流式输出工具执行阶段会推送“正在调用 XX 工具”的中间事件模型最终回答阶段会逐字推送 token。实现层面用 AsyncGenerator 来推事件流前端或服务端可以做 SSE 透传。事件类型分三类tool_start开始执行某个工具附工具名tool_end工具执行完毕附结果摘要token模型回答的增量文本多用户并发是另一个必须考虑的点。每个用户的会话是独立的状态机对话历史、调用次数、可用的工具集都不能互相串。我用了一个简单的 SessionManager以 session_id 为 key 存一个 AgentSession 实例每个实例持有自己的消息列表和计数器。class SessionManager: def __init__(self): self._sessions: dict[str, AgentSession] {} def get_or_create(self, session_id: str) - AgentSession: if session_id not in self._sessions: self._sessions[session_id] AgentSession() return self._sessions[session_id] def cleanup(self, max_idle_seconds: int 3600): # 定期清理空闲会话防止内存泄漏 ...内存泄漏这事我吃过亏。最开始不清理会话跑了几天服务内存就飙到几个 G。后来加了个后台任务每分钟扫描一次超过一小时没活动的会话直接销毁。这个清理规则在实际部署中按你自己业务的会话时长调整别一个小时后用户还在聊却被清了上下文。4. 实操部署与接入业务架构和核心实现聊完了这节说点更实际的怎么把 hermes-agent 跑起来怎么接进自己的业务。我没有把项目搞成一个 SaaS 产品它就是一个 Python 库配合 FastAPI 可以快速变成一个内部服务。4.1 环境准备与依赖选择项目依赖非常克制核心就四个依赖用途openai / anthropic / 其他 SDK具体模型接入pydantic参数校验与 schema 生成httpx内部服务 HTTP 调用python-dotenv配置管理安装就一条命令pip install hermes-agent然后配好环境变量# .env LLM_API_KEYyour_api_key_here LLM_MODELgpt-4o-mini # 或者你惯用的模型名称 MAX_ITERATIONS10我一直觉得工具链越简单越好一个库最好只专注于一件事。这也是我当初自己造轮子的原因之一——外面那些框架动不动拉几百个间接依赖安全审计的时候非常痛苦。4.2 快速搭建一个问答服务下面给一个完整的 FastAPI 集成示例如果你想展示给团队看直接把它跑起来就行。# app.py from fastapi import FastAPI, Request from pydantic import BaseModel from hermes_agent import Agent, SessionManager from hermes_agent.providers import OpenAIProvider app FastAPI() sessions SessionManager() agent Agent( providerOpenAIProvider(), tools[query_order, query_user_profile], # 这边是你注册的工具 ) class ChatRequest(BaseModel): session_id: str message: str app.post(/chat) async def chat(req: ChatRequest): session sessions.get_or_create(req.session_id) result await agent.run( sessionsession, user_inputreq.message, streamFalse, # 正式接流式会改这里 ) return {reply: result.content} if __name__ __main__: import uvicorn uvicorn.run(app, host0.0.0.0, port8080)从零到能跑大概就是上面这个过程。你需要做的只有三件事配好模型密钥、把你要用的工具函数写出来并注册进去、起一个 web 服务。4.3 接入内部工具的几个真实案例我在团队内部实际用过三种场景拿来给大家参考。场景一是内部知识库检索。我把团队的 wiki 文档做了向量化注册了一个search_docs(query)工具。用户提问时Agent 判断是否需要查文档需要就调用工具拿到相关内容后组织回答。这里有个技巧工具返回的内容要截断只返回 top-5 片段的正文摘要。不然模型要处理一大堆无关文本既费 token 又容易跑偏。场景二是数据库自然语言查询。我先定义了一个run_sql(sql)工具加上一个list_tables()工具。模型拿到用户问题会先看有哪些表再生成 SQL执行后把结果翻译成口语化回答。这段链路里有一个非常重要的安全约束run_sql只允许读操作我在工具函数里强制把 SQL 转成只读事务遇到 INSERT/UPDATE/DELETE 直接拒绝。注意凡是让大模型生成代码或 SQL 再执行的场景一定要在工具层面做白名单校验。不要指望模型“听话”只读不改防御必须落在代码里而不是提示词里。场景三是工单自动分诊。用户描述问题Agent 判断属于哪个部门、紧急程度多高然后调用create_ticket()工具自动建单。这个场景对准确性要求很高我的做法是给create_ticket工具加上confirm_before_execute标记凡是这类“有副作用”的工具都会先返回一个待确认状态由用户说“确认”之后才真正执行。这个机制在自动化程度上做了取舍但换来了安全性内部用起来放心很多。5. 常见问题与排查技巧写这个项目的过程中踩了不少坑有些问题网上讲得少我集中整理成一张速查表再挑几个典型问题详细说说排查思路。5.1 典型问题速查表现象可能原因处理方案工具被反复调用但结果没用上工具返回格式复杂模型没理解简化返回格式用纯文本或精简 JSON并附说明参数解析报错模型生成的 JSON 不合法加上尾逗号修复逻辑或者把参数类型声明得更严格对话总是遗忘前文上下文裁剪策略把关键信息裁掉了把工具执行结果标记为高优先级不参与裁剪Agent 陷入死循环工具返回错误信息后模型反复重试限制重试次数超过后主动结束并提示用户并发请求互相串消息Session 管理缺失确保每个 session 有独立的消息列表流式输出卡住SSE 连接被前置网关缓冲关闭中间层缓冲或改用 chunked transfer5.2 我最难受的一个 bug工具结果被模型无视有一次线上反馈Agent 明明调用了query_order工具参数也对返回数据也正常但最终回答里就是没有订单信息反而说“我无法查询到该订单”。查了很久才发现问题出在工具结果的 role 标记上。我最初把工具结果统一标记成role: user追加到对话里认为模型能看懂。但那个版本的消息结构里模型会把“用户消息”和“工具返回”混淆尤其当工具结果里包含“订单不存在”这类字眼时模型会当成用户自己说的话于是回一句“抱歉我无法查询”。解决办法工具结果必须用role: tool标记并且带上对应的 tool_call_id。这是 OpenAI 协议明确要求的格式照着做最简单可靠。{role: tool, tool_call_id: tool_call.id, content: json.dumps(result)}说实话这个问题在官方文档里写得很清楚但我第一次写的时候为了省事没照着做踩了坑才回头改。所以这里也提醒大家大模型的工具调用链路里有很多“协议级约定”不要自作聪明去简化简化的地方往往就是 bug 的来源。5.3 调参经验这些数字值得先跑一跑我总结几个比较好用的起始参数你可以先抄作业再调温度工具调用场景设 0 或 0.1。温度高了模型容易“发挥”生成长尾 JSON 时不稳。只有纯聊天场景才考虑 0.7 以上。最大迭代次数10 够用。如果你的工具链路特别长比如 A 工具结果要喂给 B 工具再喂给 C可以适当上调但优先考虑合并工具。超时时间单次工具调用的超时我设为 15 秒HTTP 连接 3 秒。内部服务一般响应很快15 秒已经是很保守的值。上下文保留轮数10-20 轮之间。工具密集型的任务建议 20纯问答 10 就够。这几个参数的调整思路就一句话让链路尽快、尽量稳定地走向终点而不是让模型“自由发挥”。Agent 跟人聊天不一样它的价值在于把事情办成而不是表达得精彩。5.4 上线前一定要做的验证清单最后分享一份我在上线前必过的检查清单不一定完整但每个项目我都吃到了它的好处所有工具都有超时和错误返回不会因为一个下游接口挂了就整个 Agent 卡住有副作用的工具都加了人工确认机制日志里能完整还原每一次工具调用的入参和出参会话有清理机制不会内存泄漏对恶意输入比如“忽略之前的指令”有基础的提示词防护模型返回被截断时能正确处理不会因为解析失败崩溃结尾小技巧写到这里hermes-agent 从设计到落地基本讲完了。最后再分享一个我觉得特别实用的小细节给工具加一个returns_sensitive_data标记凡是返回用户隐私数据的工具在日志里自动把内容脱敏再落盘。这样既保住了调试能力又不会把敏感信息留在日志系统里。这个想法是某次内部安全评审被提出来后加上的成本极低但上线后维护系统的人省了很多心。如果你也在折腾自己的 Agent 框架我的建议是先别急着上多复杂的规划、反思、自我改进机制那都是后面的事。先把“模型-工具-结果-再决策”这条主链路跑得稳稳当当的再考虑加戏。跑通一个端到端的工具调用比堆一百个炫酷的功能更实在。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询