
做AI Agent开发这段时间我一直围绕自建的hello-agents框架打转。说实话Agent框架搭建这件事看着简单真正跑起来全是细节。这是《探秘 AI Agent | Hello-Agents 项目学习笔记》的第六篇前五篇我记录了从环境准备到模型接入的完整过程这一篇不想再聊具体API而是想把Agent框架搭建这件事拆到根上为什么自建、核心组件怎么设计、怎么用最少的代码跑通一个可用Agent、以及长期困扰人的坑该怎么填。这篇笔记适合已经跑过几个LLM API调用但不满足于“调库”的人。如果你正在纠结LangChain、Dify、CrewAI到底选哪个看完这篇你可能会对“自建”有不一样的理解。1. 为什么自建Agent框架主流框架没说的另一面1.1 主流Agent框架的舒适区与边界过去半年我把市面上能叫得上名字的Agent框架都试了一遍包括LangChain、Dify、CrewAI、Spring AI甚至用Rust写过几个小型Agent项目。每个框架都有自己的高光时刻但也都有明显边界。框架擅长的场景我在实际使用中感受到的边界LangChain快速串联LLM调用、工具、向量库生态最全抽象层次太多出了问题要扒很多层源码版本升级破坏性大Dify偏向可视化编排适合运营快速搭工作流适合固定流程但Agent的动态决策被工作流限制住了CrewAI多Agent角色协作模拟团队分工编排约定很强想自定义一个“非典型”协作流程时很别扭Spring AIJava生态友好企业级接入方便模型支持和社区资料相对少学习曲线偏Java方向Rust Agent生态性能强、类型安全适合做运行时底座开发速度偏慢LLM生态工具链还不算成熟这些框架解决的是80%的通用问题剩下20%的“不通用”需求恰恰是项目里最挠头的地方。比如我要精确控制某个Agent的上下文窗口或者想在每次模型调用前统一注入业务侧变量主流框架并不会把这种底层逻辑直接暴露给我。再比如token成本很多框架把历史消息自动塞给你你无感知地多花了钱出了问题都不知道是框架哪一层干的。自建框架的意义不是否定这些项目而是把黑盒打开看一眼。我个人的体会是用LangChain三个月不如手写一个最小Agent循环来得通透。1.2 Hello-Agents框架的设计定位hello-agents是我自己维护的一套轻量Agent框架定位很明确不追求大而全只保留Agent运行最必要的能力。它由五个模块构成模型接入层、工具注册表、记忆管理、Agent执行循环、编排器。设计初衷源于一个很朴素的诉求——我想知道一条用户消息从进入系统到模型返回中间到底发生了什么。用别人框架时我只能通过日志猜测用自建框架时每一条消息都是我写的代码在处理。代码即文档出了问题翻代码比翻文档快得多。所以hello-agents的核心理念就是“最小可运行、显式优于隐式”一个Agent对象一个agent.run()方法中间的过程参数尽量显式暴露出来哪怕多写几行代码也不搞暗箱操作。1.3 自建框架的收益与代价收益是最直接的第一完全可控模型调用、工具执行、异常处理全部在自己手里第二深入理解Agent框架最核心的ReAct循环、Function Calling、记忆管理手写一遍之后再去看LangChain的AgentExecutor源码会轻松很多第三方便裁剪想支持CrewAI那种多角色协作自己加一个编排层就行不需要被框架的规范束缚。代价也很诚实需要自己处理模型API差异、超时重试、并发限流、token记账这些脏活累活主流框架已经帮你蹚过一遍了。如果你只是快速搭建一个Demo自建框架的性价比不高但如果你要长期维护一个Agent系统这些“脏活”迟早都得懂。我的建议是可以先自建一个最小框架做学习或内部工具再根据业务成长选择保留自建还是迁移到成熟框架。hello-agents目前就是我这个思路的产物它还很糙但足够真实。2. Agent框架搭建的核心组件一个也不能少2.1 Agent运行循环五步理解ReActAgent框架最核心的部分是执行循环业界叫ReAct模式网上常说的“react agent框架图”其实就是下面这个循环接收任务和系统提示词模型推理Thought决定下一步做什么调用动作Action选择并调用工具观察结果Observation拿到工具返回信息判断是否结束给不出最终答案就继续循环用伪代码表达一个最小Agent循环只有十几行def run(task: str): messages [system_message, user_message(task)] for step in range(max_steps): response llm.chat(messages) if response.is_final_answer: return response.content action response.tool_call observation execute_tool(action) messages.append(observation_message(observation)) return MAX_STEPS_REACHED这段逻辑看起来简单但整个框架搭建的复杂度都藏在这个循环的细节里解析模型输出、匹配工具参数、处理工具异常、控制上下文长度、避免死循环。2.2 模型接入层与token管理很多初学者问“ai agent token是什么意思”在Agent框架里token就是模型处理文本的最小单位你的每一次推理、每一个工具调用结果、每一段历史消息最后都会折算成token计费。模型接入层要做的第一件事是把OpenAI、Gemini、Claude这些厂商的差异“抹平”。在我自建的hello-agents里定义了一个ChatModel接口统一接收消息列表和参数返回标准化的模型响应。底层再接不同厂商的SDK上层不用关心。token管理则是另一件容易被忽视的事。Agent每多跑一轮历史消息就会膨胀一轮成本是线性甚至指数上升的。所以hello-agents做了一个上下文管理器负责三件事统计当前轮次的输入和输出token估算费用超出窗口时自动裁剪最早的历史对长工具结果做摘要压缩。实际测试下来同样一个检索类任务有了这层管理后token消耗能下降30%到50%。2.3 工具层与Function Calling如果说Agent循环是骨架工具层就是血肉。hello-agents里工具的注册不是简单定义函数而是要生成一份模型可以理解的JSON Schema。以OpenAI Function Calling为例你希望Agent调用一个搜索文档的工具需要这样注册tool(search_docs) def search_docs(query: str, max_results: int 5) - str: 根据关键词搜索项目文档返回匹配内容 # ...实际检索逻辑框架会解析这个函数的签名、类型、docstring自动转成tools参数传给模型。模型看到这个schema后如果发现需要检索会返回一个结构化的调用请求{ name: search_docs, arguments: {\query\: \Agent框架搭建\, \max_results\: 3} }框架要做的是把arguments解析成Python参数调用函数把结果追加回对话。这个链路看着不复杂但它决定了Agent能不能“下地干活”。我在实际使用中强烈建议工具函数的docstring一定要写清楚“什么时候用”以及“参数边界”模型对工具的误判80%都是因为说明书写得含糊。2.4 记忆与状态管理Agent框架搭建里记忆模块最容易被新手跳过但恰恰是它决定了Agent是“有脑子的助手”还是“每句话翻篇的鹦鹉”。Hello-agents把记忆分成两层短期记忆和长期记忆。短期记忆直接复用对话上下文靠消息列表携带核心是控制长度与相关性。长期记忆则会抽取出关键实体、偏好、历史决策存入向量库在每次任务开始时检索与当前问题相关的片段作为额外上下文注入。这里有一个很大的坑状态管理的边界。如果Agent在执行多步工具调用时某一步突然抛异常整个状态是应该回滚、重试还是放弃我最初的实现是直接抛出异常结果经常导致Agent任务中断。后来我改成在循环内捕获异常把错误信息当作一次observation返回给模型让模型自行判断下一步。这个改动让任务成功率明显提升因为模型往往能从错误信息里找到原因。2.5 编排层单Agent到多Agent协作自建框架做到第二个版本你就会发现单Agent能力存在天花板既要做分析、又要写代码、还要跑测试上下文很快被占满工具切换也容易出错。这时候需要引入编排层也就是让多个Agent各司其职、协同工作。CrewAI的“角色-任务-流程”思想值得借鉴。我把hello-agents的编排器设计成一张有向图节点是不同Agent边是消息传递关系。比如一个简单的“需求分析流程”可以定义需求Agent先解析需求产出结构化任务清单然后开发Agent消费任务清单产出代码最后测试Agent消费代码产出测试报告。这个设计比单Agent循环复杂但本质逻辑一致每个子Agent都有自己的system prompt、工具集合、循环上限编排器只负责调度和传递上下文。先别急着加多Agent单Agent跑不稳多Agent只会把错误放大三倍。3. 实操基于Hello-Agents搭一套可运行流程3.1 目录结构与接口设计hello-agents目前的目录结构是这样拆的hello_agents/ ├── agent.py # Agent核心类与执行循环 ├── model.py # 模型接入统一接口 ├── context.py # 上下文与token管理 ├── memory.py # 短期/长期记忆 ├── tools/ │ ├── registry.py # 工具注册表 │ └── builtin.py # 内置常用工具 ├── orchestration.py # 多Agent编排器 └── server.py # FastAPI对外服务这种划分的核心原则是“单一职责”模型、工具、记忆、执行、编排互不感知实现细节只通过接口通信。好处是换一个模型接入、加一个新工具都不用动Agent循环。坏处是初期要多写两层包装代码不过这个付出很值得。3.2 手写ReAct循环先让框架跑起来下面这段代码是hello-agents最早的Agent核心精简后大概五十行但它足以跑通一个基础的Agent流程。实现思路参考ReAct循环但我在里面加了三个关键控制最大步数、重复检测、异常恢复。class Agent: def __init__(self, model, tools, max_steps8): self.model model self.tools tools self.max_steps max_steps self.history [] def run(self, task: str) - str: self.history [system_message, user_message(task)] last_observations [] for step in range(self.max_steps): response self.model.chat(self.history) if response.type final: return response.content tool_name response.tool_call.name arguments json.loads(response.tool_call.arguments) try: observation self.tools.execute(tool_name, arguments) except Exception as e: observation f工具执行失败: {type(e).__name__}: {e} # 重复检测如果连续三次出现相同工具和相同参数直接打断 last_observations.append((tool_name, arguments)) if len(last_observations) 3 and len(set(map(str, last_observations[-3:]))) 1: return Agent陷入重复调用已中断 self.history.append(assistant_message(response.content, tool_callresponse.tool_call)) self.history.append(tool_message(observation, nametool_name)) last_observations.append(observation) return 超过最大执行步数这段代码最重要的不是实现本身而是它揭示了一个事实Agent框架搭建的核心不是炫技而是把“循环控制”做好。你在生产环境里遇到的大部分问题无外乎循环出不去、重复执行、工具调用失败只要在循环上补好刹车框架就已经及格了一半。3.3 让Agent下地干活工具接入与安全边界框架跑通后就要让它真的“下地干活”。我接的第一个实用工具是内部文档搜索第二个是SQL查询库。这两类工具都有安全隐患所以框架必须做隔离和限制我总结了三条铁律工具必须显式注册不允许Agent动态生成工具函数。所有工具执行前要校验参数类型和取值范围比如SQL工具只允许SELECT不允许DELETE。工具执行要有超时控制默认10秒超时直接返回错误不让模型挂在那里干等。实际运行的坑在于模型经常会产生“幻觉参数”。比如文档搜索工具支持日期范围模型会自作主张把当前日期当参数传进去而框架记录里根本没有这个值。后来我加了参数过滤对于模型未提供的参数一律使用工具的默认值不进行二次推断。安全边界上还要注意Agent可以调用工具拿到真实数据但最终对外输出前应该有一个审核层。尤其是涉及企业内部数据的场景不能盲目相信模型生成的内容。hello-agents目前的做法是给所有工具返回的数据打上“数据来源”标签后续可以做引用溯源。3.4 从同步到异步并发与稳定性改造最早版本的hello-agents是同步实现一次只能处理一个任务慢得像单线程隧道。后来我接了一个小工具需要给业务部门批量处理几十条文本才意识到“ai agent怎么扛并发”这个问题有多迫切。我的改造方向是asyncio加信号量import asyncio from asyncio import Semaphore async def run_many(tasks, model, tools, limit5): sem Semaphore(limit) async def bounded(task): async with sem: agent Agent(modelmodel, toolstools) return await asyncio.to_thread(agent.run, task) # 同步循环放入线程池 results await asyncio.gather(*[bounded(t) for t in tasks]) return results这里有三个容易被忽略的点。第一很多模型SDK的聊天接口是同步阻塞的直接放在async事件循环里会把整个循环卡死。我的做法是用asyncio.to_thread把同步逻辑丢到线程池保持接口响应不阻塞。第二并发上限不是越高越好要参照模型API的限流额度。我试过把并发调到20结果一分钟后被限流后面全是429。正确姿势是先查API文档确认RPM和TPM限制再设置合理的semaphore。第三并发场景下要做好请求级别的隔离。每个任务的history不能共享模型实例也不能共用否则一个任务里的上下文会污染另一个任务。我在实际开发中因为这些共享变量查了一整天才定位到问题。3.5 接入FastAPI对外服务并发改造完之后下一步就是提供服务。hello-agents的对外服务我用的是FastAPI因为它和asyncio天然契合而且自带请求校验和文档。核心接口就两个提交任务、查询任务结果。由于Agent任务执行时间可能很长所以没有用同步请求等待结果而是采用异步任务队列收到HTTP请求后创建任务ID返回202后台执行Agent循环执行完成后把结果写入内存或Redis前端轮询结果。这一步可以参考FastAPI的BackgroundTasks也能用Celery做更可靠的任务分发。贴一个最简化版的服务代码from fastapi import FastAPI, BackgroundTasks from pydantic import BaseModel app FastAPI() tasks_store {} class TaskIn(BaseModel): content: str app.post(/agent/task) async def create_task(payload: TaskIn, background_tasks: BackgroundTasks): task_id str(uuid.uuid4()) tasks_store[task_id] {status: running, result: None} background_tasks.add_task(execute_bg, task_id, payload.content) return {task_id: task_id} async def execute_bg(task_id, content): result await agent.run_async(content) tasks_store[task_id] {status: done, result: result}实际部署时还可以在FastAPI前加一层简单限流中间件防止外部请求把模型额度打爆。这个服务上线后我自己的体验是API调用方不用关心Agent内部有多少步只需要拿到任务ID和最终结果调用关系清爽很多。4. 常见问题与排查技巧实录4.1 Agent陷入无效循环怎么破Agent框架搭建中最常见的问题是模型在循环里反复调用同一个工具或者答非所问。排查思路分三步。第一步确认步数上限有没有设置。很多“无限循环”其实是忘了配max_steps模型为了凑最终答案会一直尝试。第二步观察连续几步的observation是否高度相似。如果是大概率是模型没有从工具返回中得到新信息可能原因有工具返回的内容太冗余模型被噪声淹没或者工具查询参数本来就有问题结果每次返回都是同一批数据。第三步检查输出格式解析。OpenAI模型偶尔会在函数调用之外额外输出解释性文本如果框架的解析逻辑只认严格JSON就会出现“解析失败-重试-又失败”的死循环。解决办法是增加容错解析先尝试JSON解析失败就用正则提取参数块。我在hello-agents里还加了一个特殊机制如果检测到“工具名参数”的组合连续出现三次就判定为无效循环主动终止并返回“当前Agent无法解决该任务”。宁可让它承认失败也不让它烧掉几十轮token。4.2 token消耗像流水一样怎么控token失控是最让团队肉疼的问题。一个看起来简单的任务可能背后跑了十几轮工具调用每个工具结果都带着几千字上下文返回最后账单出来吓一跳。这里的核心不是换便宜模型而是控制上下文增长。我现在的做法有三个历史消息压缩超过3轮对话就触发一次摘要用摘要替换最早的具体内容工具结果截断每个工具返回只保留前2000字符超出部分加一个“内容过长已截断”的标记限制并行推理轮数如果任务没有新的上下文输入模型调用的步数上限可以收紧。还要特别关注“隐式token开销”。很多框架会在系统提示词里塞一大堆工具说明每个工具的schema都完整展开这部分token虽然看不见但每次请求都在消耗。hello-agents做了工具按需挂载根据任务关键词预选出可能用到的三个工具只给模型暴露这三个工具的schema实测可减少20%左右的输入token。4.3 并发场景真的扛不住吗当你把Agent接到真实业务里“ai agent怎么扛并发”就会变成一个避不开的话题。但我的真实感受是多数场景下的瓶颈不在Agent框架本身而在模型API的限流。框架再怎么优化也突破不了“每秒请求数”和“每分钟token数”这两个硬限制。所以扛并发的核心是“削峰填谷”用队列缓冲请求用并发控制限制同时打到API的请求数用退避重试处理429和超时。具体参数可以参考这张速查表参数推荐值说明最大并发数根据API RPM/3预留部分配额给重试和突发超时时间30秒超过后终止本次模型请求重试次数最多3次采用指数退避1秒-2秒-4秒队列长度100超过直接拒绝新任务保护系统如果你的项目对延迟极度敏感并发冲击又大我可以坦诚地说Python Agent在高并发下的性能上限不高。这时候可以考虑把Agent核心循环迁移到Rust或者至少把工具调用、上下文管理这种重计算模块用Rust重写为原生扩展我做过一个实验单Agent循环在Rust下的开销比Python低一个数量级当然开发时间也成倍增加。4.4 和LangChain、Dify、CrewAI对比后的选择建议这段时间反复折腾自建框架我对“agent框架如langchain、dify、crewai等哪个好”这个问题的回答已经变了先看你的需求是“要一个系统”还是“要一个自己掌握的系统”。如果是业务要快速跑通一个问答机器人Dify的可视化编排最合适如果团队已经深度使用LangChain的生态比如大量用它的文档加载器和向量库那就继续用LangChain没必要推倒重来如果重点是多个角色协作CrewAI的封装能省很多事如果你在Java技术栈里Spring AI是稳妥选择。反过来说如果你需要反复调整Agent的决策逻辑、精细控制token成本、或者把Agent嵌入到一个定制化很强的业务系统里自建框架反而更顺手。hello-agents和它们的最大区别是我对每一行执行代码都了如指掌出了问题能直接修而不是等框架作者发版本。这里还想补充一句不要把“自建”理解成“从零造轮子”。我在hello-agents里也借鉴了很多成熟框架的思路比如CrewAI的编排思想、LangChain的工具Schema设计复用思想不丢人丢掉思考才可惜。4.5 后续扩展方向hello-agents目前的版本还处于“个人生产可用”阶段接下来我计划做四件事。第一更完善的多Agent编排目前的编排器只支持简单链式调用下一步会支持并行执行和条件分支。第二增加多模态输入支持让Agent能处理图片和语音这需要模型接入层做一轮重构。第三把核心执行循环用Rust重写为网络服务Python侧只做调度解决性能焦虑。第四补充更细粒度的审计日志每个Agent每轮的token消耗、工具调用、决策原因都能可视化回溯。如果你也是自建Agent框架我建议从“给框架写文档”开始。hello-agents每新增一个能力我就先写设计文档再说代码。这套流程意外地好用因为写文档的过程会逼你想清楚这个模块到底为什么存在。最后分享一个很个人的体会Agent框架搭建一半是工程一半是认知。工程上无非是模型、工具、记忆、循环、编排这几件事认知上却要不断在“自主性”和“可控性”之间找平衡。框架太激进Agent会像脱缰的野马乱跑框架太死板Agent又失去了智能决策的意义。hello-agents目前的答案很简单框架只做流程控制不限制模型思考工具只做能力边界不干预模型判断。你先把这个平衡找到再谈复杂功能也不迟。