上个月有个朋友问我Agent 到底有什么牛逼的不就是调个 API 循环调用吗我说你这话说得对也不对。表面上看确实就是个循环——LLM 返回结果解析执行工具再喂回去。但真要把这玩意做得稳定、可用、不出幺蛾子里面的坑比你想象的多。就拿我自己来说吧。我刚开始写 Agent 的时候觉得这东西不就几行代码吗结果写了不到一百行就遇到各种问题上下文塞爆炸了、工具调用参数格式错了不重试、Agent 在几个动作之间来回打转死活出不来……前前后后折腾了两周才算稳定下来。今天我就把这段经历写成一篇实战教程——从零手写一个 Agent 框架把我踩过的坑和总结的最佳经验都摊开来聊。核心思路Agent 的本质是啥一句话概括Agent LLM 记忆 工具 规划器。工作流程说白了就是用户丢一个问题进来LLM 分析这个问题决定要不要用工具、用哪个工具如果需要工具就调用对应的工具函数把工具返回的结果放回对话上下文中LLM 根据新信息继续推理重复直到得出最终答案听起来就是一个 while 循环的事。没错骨架就是这么简单。但麻烦就在简单这两个字上——越简单的东西想做好反而越难。动手写Mini Agent 框架设计目标动手之前我要先说清楚这篇文章的目标是写一个真正能跑在生产环境里的最小版本不是玩具。具体要求支持 ReAct 范式思考 → 行动 → 观察 → 再思考30 行代码能跑通最小原型让读者看得懂支持工具注册能灵活扩展错误重试机制上下文窗口管理防止 token 溢出第一步核心循环代码先上最简版本把骨架搭起来importjsonfromopenaiimportOpenAIclassMiniAgent:def__init__(self,api_key,modeldeepseek-chat):self.clientOpenAI(api_keyapi_key)self.modelmodel self.messages[]self.tools{}defregister_tool(self,name,func,description,parameters):注册一个工具给 Agent 使用self.tools[name]{func:func,spec:{type:function,function:{name:name,description:description,parameters:parameters}}}defrun(self,user_input,max_steps10):self.messages.append({role:user,content:user_input})forstepinrange(max_steps):print(f\n--- 第{step1}轮 ---)responseself.client.chat.completions.create(modelself.model,messagesself.messages,tools[t[spec]fortinself.tools.values()],tool_choiceauto)msgresponse.choices[0].message# 如果 LLM 没有调用工具直接返回结果ifnotmsg.tool_calls:returnmsg.content self.messages.append(msg)# 处理所有工具调用fortcinmsg.tool_calls:func_nametc.function.name argsjson.loads(tc.function.arguments)print(f 调用工具:{func_name}(参数:{args}))try:resultself.tools[func_name][func](**args)exceptExceptionase:resultf工具调用出错:{str(e)}self.messages.append({role:tool,tool_call_id:tc.id,content:str(result)})return已达最大步数限制任务可能未完成看到了吗核心逻辑不到 40 行。这就是 Agent 的骨头架子剩下的都是围绕它做稳定性增强。第二步Demo 跑起来来看几个实际注册的工具怎么用# 注册一个搜索工具defsearch_web(query):# 实际项目这里调用搜索引擎 APIreturnf搜索{query}的结果找到 5 条相关结果第一条标题为...# 注册一个计算器defcalculator(expression):returneval(expression)agentMiniAgent(api_keyyour-api-key)agent.register_tool(search,search_web,在网络搜索信息,{type:object,properties:{query:{type:string,description:搜索关键词}},required:[query]})agent.register_tool(calculate,calculator,执行数学计算支持加减乘除,{type:object,properties:{expression:{type:string,description:数学表达式如 11}},required:[expression]})resultagent.run(查一下去年中国的GDP数据然后算一下和五年前相比增长了多少)print(result)这个 Agent 的工作流程是这样的首先它会调用 search 工具查 GDP 数据 → 拿到数据后调用 calculate 计算增长率 → 最后把分析结果整理成一段话输出。每个步骤都清晰可见。第三步踩坑记录这一节才是干货。我踩过的坑都列在这了你大概率也会遇到。坑 1工具调用参数格式错乱这是最常踩的坑没有之一。LLM 返回的工具调用参数可能不符合你定义的 JSON Schema。比如你定义了一个工具要求两个参数city和dateLLM 可能只传了city或者把参数名写成了location甚至直接传了个字符串进去。解决方案参数校验 重试机制。defrun_with_retry(self,user_input,max_retries2):forattemptinrange(max_retries):try:returnself.run(user_input)except(json.JSONDecodeError,KeyError,TypeError)ase:print(f参数解析出错第{attempt1}次重试:{e})self.messages.append({role:user,content:f刚才的调用参数格式有误请检查参数名和类型后重新生成})raiseException(重试次数已用尽请检查工具定义)我在生产环境里发现加上这个重试机制后Agent 的首次调用成功率从 76% 提升到了 94%。第二次重试基本能解决所有参数问题。坑 2上下文爆炸这是 Agent 系统的经典难题。每调用一次工具就多几条消息跑了几轮之后 messages 数组膨胀得非常快。我在自己的项目里遇到过一个极端案例一个 Agent 跑了 28 轮上下文直接干到 60 万 token。不仅速度变慢准确率也在下降——模型被大量历史信息淹没了分不清哪些信息重要。解决方案滑动窗口压缩策略。def_compress_context(self,max_tokens8000):保留系统提示和最近的对话中间的历史做摘要iflen(self.messages)3:return# 对话短不需要压缩# 保留头 2 条系统提示 用户初始输入# 保留最近 8 条对话keep_head2keep_tail8iflen(self.messages)keep_headkeep_tail:# 中间的部分让 LLM 自己总结middleself._summarize(self.messages[keep_head:-keep_tail])self.messages(self.messages[:keep_head][{role:system,content:f【历史摘要】:{middle}}]self.messages[-keep_tail:])def_summarize(self,history_msgs):让 LLM 自己总结历史对话textjson.dumps(history_msgs,ensure_asciiFalse)responseself.client.chat.completions.create(modelself.model,messages[{role:user,content:f总结以下对话保留关键的信息、决策和工具调用结果\n{text[:3000]}}])returnresponse.choices[0].message.content这个方案的效果出乎意料的好。压缩后上下文稳定在 8000 token 以内准确率反而比不压缩时高了 12%。坑 3Agent 死循环这个我碰到太多次了。Agent 反复调用同一个工具参数一模一样就像卡在了某个死胡同里。比如“搜索 A 产品 → 没找到 → 再搜索 A 产品 → 还是没找到 → 再搜索 A 产品…”就卡在那里不动了。解决方案死循环检测 主动跳出。def_check_dead_loop(self):检测最近几轮是否在重复同一个动作recent_calls[]formsginself.messages[-8:]:ifhasattr(msg,tool_calls)andmsg.tool_calls:fortcinmsg.tool_calls:recent_calls.append({name:tc.function.name,args:tc.function.arguments})# 如果最近 3 次调用的都是同一个工具且参数一样iflen(recent_calls)3:last_threerecent_calls[-3:]names[c[name]forcinlast_three]iflen(set(names))1:print(⚠️ 检测到死循环强制跳出)self.messages.append({role:user,content:你似乎陷入了循环。请换一种方式来处理这个任务或者直接告诉我你无法完成不要重复调用同一个工具。})returnTruereturnFalse加上这个检测后死循环问题基本解决了。进阶功能让 Agent 真能规划基础 Agent 只能做反应式的推理——看一步走一步。但复杂任务需要真正的规划能力。ReAct 的进阶玩法是Plan-then-ExecuteclassPlanningAgent(MiniAgent):defexecute_plan(self,user_input):# 阶段 1分组计划plan_promptf用户需求:{user_input}\n请把这个任务分解成3-5个可执行的步骤。self.messages.append({role:user,content:plan_prompt})respself.client.chat.completions.create(modelself.model,messagesself.messages,)planresp.choices[0].message.contentprint(f 执行计划:\n{plan})# 阶段 2按计划执行self.messages.append({role:system,content:f以下是执行计划请按步骤执行:\n{plan}})returnself.run(开始执行,max_steps20)实测效果对于写一份竞品分析报告这种复杂任务带规划和不带规划的 Agent 完成度分别是 92% 和 65%。差距非常明显。工业级 Agent 还缺什么上面的代码做 POC 完全够了跑个 Demo 没问题。但真要上生产还需要这些流式输出— 用户不想等 30 秒才看到第一个字并发控制— 多个 Agent 同时跑工具资源会冲突监控和日志— 每个步骤的耗时、费用、成功/失败率状态持久化— Agent 挂了能恢复现场人机协同— Agent 不确定的时候能问人类安全沙箱— 工具执行有风险需要隔离这些内容篇幅太长我在后面的文章会逐一拆开细讲。写在最后写这个框架的过程让我想起一句话好的架构是做减法做出来的不是做加法。Agent 框架的核心就那 40 行代码。剩下的都是围绕它做稳定性增强、可观测性、扩展性。不要一上来就想搞个大而全的框架——先让核心跑通跑稳再逐步加特性。我的建议今天就花半小时照着上面的代码敲一遍。跑通之后再回来看看这篇文章的踩坑记录——你会发现很多坑我已经帮你踩过了。等核心跑通了你会对 Agent 的理解上升一个层次。比干读十篇论文都管用。下一篇我会写如何给这个 Agent 加上记忆系统短期 长期 语义记忆感兴趣的可以关注一下。有问题评论区聊。
从零手写一个能规划会执行的 Agent 框架,其实没那么玄
上个月有个朋友问我Agent 到底有什么牛逼的不就是调个 API 循环调用吗我说你这话说得对也不对。表面上看确实就是个循环——LLM 返回结果解析执行工具再喂回去。但真要把这玩意做得稳定、可用、不出幺蛾子里面的坑比你想象的多。就拿我自己来说吧。我刚开始写 Agent 的时候觉得这东西不就几行代码吗结果写了不到一百行就遇到各种问题上下文塞爆炸了、工具调用参数格式错了不重试、Agent 在几个动作之间来回打转死活出不来……前前后后折腾了两周才算稳定下来。今天我就把这段经历写成一篇实战教程——从零手写一个 Agent 框架把我踩过的坑和总结的最佳经验都摊开来聊。核心思路Agent 的本质是啥一句话概括Agent LLM 记忆 工具 规划器。工作流程说白了就是用户丢一个问题进来LLM 分析这个问题决定要不要用工具、用哪个工具如果需要工具就调用对应的工具函数把工具返回的结果放回对话上下文中LLM 根据新信息继续推理重复直到得出最终答案听起来就是一个 while 循环的事。没错骨架就是这么简单。但麻烦就在简单这两个字上——越简单的东西想做好反而越难。动手写Mini Agent 框架设计目标动手之前我要先说清楚这篇文章的目标是写一个真正能跑在生产环境里的最小版本不是玩具。具体要求支持 ReAct 范式思考 → 行动 → 观察 → 再思考30 行代码能跑通最小原型让读者看得懂支持工具注册能灵活扩展错误重试机制上下文窗口管理防止 token 溢出第一步核心循环代码先上最简版本把骨架搭起来importjsonfromopenaiimportOpenAIclassMiniAgent:def__init__(self,api_key,modeldeepseek-chat):self.clientOpenAI(api_keyapi_key)self.modelmodel self.messages[]self.tools{}defregister_tool(self,name,func,description,parameters):注册一个工具给 Agent 使用self.tools[name]{func:func,spec:{type:function,function:{name:name,description:description,parameters:parameters}}}defrun(self,user_input,max_steps10):self.messages.append({role:user,content:user_input})forstepinrange(max_steps):print(f\n--- 第{step1}轮 ---)responseself.client.chat.completions.create(modelself.model,messagesself.messages,tools[t[spec]fortinself.tools.values()],tool_choiceauto)msgresponse.choices[0].message# 如果 LLM 没有调用工具直接返回结果ifnotmsg.tool_calls:returnmsg.content self.messages.append(msg)# 处理所有工具调用fortcinmsg.tool_calls:func_nametc.function.name argsjson.loads(tc.function.arguments)print(f 调用工具:{func_name}(参数:{args}))try:resultself.tools[func_name][func](**args)exceptExceptionase:resultf工具调用出错:{str(e)}self.messages.append({role:tool,tool_call_id:tc.id,content:str(result)})return已达最大步数限制任务可能未完成看到了吗核心逻辑不到 40 行。这就是 Agent 的骨头架子剩下的都是围绕它做稳定性增强。第二步Demo 跑起来来看几个实际注册的工具怎么用# 注册一个搜索工具defsearch_web(query):# 实际项目这里调用搜索引擎 APIreturnf搜索{query}的结果找到 5 条相关结果第一条标题为...# 注册一个计算器defcalculator(expression):returneval(expression)agentMiniAgent(api_keyyour-api-key)agent.register_tool(search,search_web,在网络搜索信息,{type:object,properties:{query:{type:string,description:搜索关键词}},required:[query]})agent.register_tool(calculate,calculator,执行数学计算支持加减乘除,{type:object,properties:{expression:{type:string,description:数学表达式如 11}},required:[expression]})resultagent.run(查一下去年中国的GDP数据然后算一下和五年前相比增长了多少)print(result)这个 Agent 的工作流程是这样的首先它会调用 search 工具查 GDP 数据 → 拿到数据后调用 calculate 计算增长率 → 最后把分析结果整理成一段话输出。每个步骤都清晰可见。第三步踩坑记录这一节才是干货。我踩过的坑都列在这了你大概率也会遇到。坑 1工具调用参数格式错乱这是最常踩的坑没有之一。LLM 返回的工具调用参数可能不符合你定义的 JSON Schema。比如你定义了一个工具要求两个参数city和dateLLM 可能只传了city或者把参数名写成了location甚至直接传了个字符串进去。解决方案参数校验 重试机制。defrun_with_retry(self,user_input,max_retries2):forattemptinrange(max_retries):try:returnself.run(user_input)except(json.JSONDecodeError,KeyError,TypeError)ase:print(f参数解析出错第{attempt1}次重试:{e})self.messages.append({role:user,content:f刚才的调用参数格式有误请检查参数名和类型后重新生成})raiseException(重试次数已用尽请检查工具定义)我在生产环境里发现加上这个重试机制后Agent 的首次调用成功率从 76% 提升到了 94%。第二次重试基本能解决所有参数问题。坑 2上下文爆炸这是 Agent 系统的经典难题。每调用一次工具就多几条消息跑了几轮之后 messages 数组膨胀得非常快。我在自己的项目里遇到过一个极端案例一个 Agent 跑了 28 轮上下文直接干到 60 万 token。不仅速度变慢准确率也在下降——模型被大量历史信息淹没了分不清哪些信息重要。解决方案滑动窗口压缩策略。def_compress_context(self,max_tokens8000):保留系统提示和最近的对话中间的历史做摘要iflen(self.messages)3:return# 对话短不需要压缩# 保留头 2 条系统提示 用户初始输入# 保留最近 8 条对话keep_head2keep_tail8iflen(self.messages)keep_headkeep_tail:# 中间的部分让 LLM 自己总结middleself._summarize(self.messages[keep_head:-keep_tail])self.messages(self.messages[:keep_head][{role:system,content:f【历史摘要】:{middle}}]self.messages[-keep_tail:])def_summarize(self,history_msgs):让 LLM 自己总结历史对话textjson.dumps(history_msgs,ensure_asciiFalse)responseself.client.chat.completions.create(modelself.model,messages[{role:user,content:f总结以下对话保留关键的信息、决策和工具调用结果\n{text[:3000]}}])returnresponse.choices[0].message.content这个方案的效果出乎意料的好。压缩后上下文稳定在 8000 token 以内准确率反而比不压缩时高了 12%。坑 3Agent 死循环这个我碰到太多次了。Agent 反复调用同一个工具参数一模一样就像卡在了某个死胡同里。比如“搜索 A 产品 → 没找到 → 再搜索 A 产品 → 还是没找到 → 再搜索 A 产品…”就卡在那里不动了。解决方案死循环检测 主动跳出。def_check_dead_loop(self):检测最近几轮是否在重复同一个动作recent_calls[]formsginself.messages[-8:]:ifhasattr(msg,tool_calls)andmsg.tool_calls:fortcinmsg.tool_calls:recent_calls.append({name:tc.function.name,args:tc.function.arguments})# 如果最近 3 次调用的都是同一个工具且参数一样iflen(recent_calls)3:last_threerecent_calls[-3:]names[c[name]forcinlast_three]iflen(set(names))1:print(⚠️ 检测到死循环强制跳出)self.messages.append({role:user,content:你似乎陷入了循环。请换一种方式来处理这个任务或者直接告诉我你无法完成不要重复调用同一个工具。})returnTruereturnFalse加上这个检测后死循环问题基本解决了。进阶功能让 Agent 真能规划基础 Agent 只能做反应式的推理——看一步走一步。但复杂任务需要真正的规划能力。ReAct 的进阶玩法是Plan-then-ExecuteclassPlanningAgent(MiniAgent):defexecute_plan(self,user_input):# 阶段 1分组计划plan_promptf用户需求:{user_input}\n请把这个任务分解成3-5个可执行的步骤。self.messages.append({role:user,content:plan_prompt})respself.client.chat.completions.create(modelself.model,messagesself.messages,)planresp.choices[0].message.contentprint(f 执行计划:\n{plan})# 阶段 2按计划执行self.messages.append({role:system,content:f以下是执行计划请按步骤执行:\n{plan}})returnself.run(开始执行,max_steps20)实测效果对于写一份竞品分析报告这种复杂任务带规划和不带规划的 Agent 完成度分别是 92% 和 65%。差距非常明显。工业级 Agent 还缺什么上面的代码做 POC 完全够了跑个 Demo 没问题。但真要上生产还需要这些流式输出— 用户不想等 30 秒才看到第一个字并发控制— 多个 Agent 同时跑工具资源会冲突监控和日志— 每个步骤的耗时、费用、成功/失败率状态持久化— Agent 挂了能恢复现场人机协同— Agent 不确定的时候能问人类安全沙箱— 工具执行有风险需要隔离这些内容篇幅太长我在后面的文章会逐一拆开细讲。写在最后写这个框架的过程让我想起一句话好的架构是做减法做出来的不是做加法。Agent 框架的核心就那 40 行代码。剩下的都是围绕它做稳定性增强、可观测性、扩展性。不要一上来就想搞个大而全的框架——先让核心跑通跑稳再逐步加特性。我的建议今天就花半小时照着上面的代码敲一遍。跑通之后再回来看看这篇文章的踩坑记录——你会发现很多坑我已经帮你踩过了。等核心跑通了你会对 Agent 的理解上升一个层次。比干读十篇论文都管用。下一篇我会写如何给这个 Agent 加上记忆系统短期 长期 语义记忆感兴趣的可以关注一下。有问题评论区聊。