做 AI Agent 这一年多,我发现不管是一千行代码还是几万行代码的系统,本质都是在跑同一个东西:一个不断循环的“感知 → 思考 → 行动 → 观察”过程。很多人一上来就研究 LangGraph、Spring AI、Rust 并发,把架构搭得特别大,结果连最基本的循环都跑不稳。这篇文章就聊清楚两件事:AI Agent 的最小循环是什么,以及怎么从最小循环走向可靠系统。
这适合两类人看:一是刚入门 Agent,被各种概念绕晕,想搞明白底层逻辑的朋友;二是已经在做项目,但总遇到死循环、超时、并发一高就崩,想从工程角度夯实基础的开发者。我会把核心原理、最小代码实现、工程化手段和常见坑位全部拆开讲,顺便穿插一些框架选型经验。
1. 先理解 AI Agent 的最小循环
1.1 Agent 到底是个什么东西
Agent 不是科幻电影里那种全能机器人。在当下的技术语境里,它就是一个“大模型 + 工具 + 环境”的组合体,通过循环决策去完成一个用户目标。大模型负责思考,工具负责执行,环境是工具所作用的外部世界,比如数据库、网页、文件系统、某个 API。
举一个生活化的类比:做菜。你是一个 Agent,头脑里的烹饪知识是大模型,菜刀、锅、灶台是工具,食材是环境。你接到“做一盘番茄炒蛋”的指令后,会先看冰箱里有什么(感知),然后决定先切番茄还是先打蛋(思考),接着动手操作(行动),看菜的状态再决定下一步(观察)。这个循环反复进行,直到出锅。
AI Agent 的最小循环就是把这个过程交给代码和大模型来跑。拆开看,每个 Agent 系统都在做四件事:接收信息、生成决策、执行动作、把执行结果放回上下文。理解了这个,后面所有复杂架构都只是在这个最小循环上做工程强化。
1.2 最小循环的四个环节
我用工程语言重新描述一遍:
- 感知:把用户请求、历史对话、环境观察到的内容组装成上下文,交给模型。
- 思考:模型基于上下文输出下一步动作,可能是调用工具,也可能是直接回答。
- 行动:如果模型决定调用工具,程序就去执行真实函数(查天气、写文件、查数据库等)。
- 观察:把工具执行结果作为新的消息写回上下文,再回到感知环节。
这四个环节里,“思考”看起来像黑盒,实际上模型的输出会被约束成一个结构化的工具调用指令,比如“调用函数 get_weather,参数是 city=北京”。程序解析这个指令,执行对应的 Python 函数,再把返回值塞回消息列表,模型看到工具结果后继续决策。
在这个循环里,上下文就是 Agent 的“短期记忆”,每轮都会追加新的消息。如果工具结果很大,或循环次数太多,上下文会被撑爆,这也是后面要说到的上下文管理问题的起源。
1.3 最小实现:几十行代码跑通一个 Agent
别急着上框架,先自己手写一个最小循环,这是理解 Agent 最有效的方式。我用 Python 加 OpenAI SDK 做了个极简版,换成其他模型接口逻辑也一样:
from openai import OpenAI client = OpenAI() tools = [ { "type": "function", "function": { "name": "get_weather", "description": "获取指定城市的天气", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "城市名"} }, "required": ["city"] } } } ] def get_weather(city: str) -> str: # 这里替换成真实天气 API return f"{city}今天晴天,气温25度" messages = [{"role": "user", "content": "北京今天天气怎么样?需要带伞吗?"}] for _ in range(10): # 设置最大循环次数,防止死循环 response = client.chat.completions.create( model="gpt-4o", messages=messages, tools=tools, ) msg = response.choices[0].message messages.append(msg) if not msg.tool_calls: print(msg.content) break for tool_call in msg.tool_calls: if tool_call.function.name == "get_weather": import json args = json.loads(tool_call.function.arguments) result = get_weather(args["city"]) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": result })这段代码已经构成一个完整的 Agent 闭环。注意几个关键点:模型返回的 tool_calls 包含函数名和参数,参数是 JSON 字符串,必须解析后才能执行;工具执行结果要以 role 为 “tool” 的消息追加,并且带上了 tool_call_id 做关联;循环必须有上限,否则模型一直在调用工具就崩了。
这个最小实现能跑通之后,你会直观感受到 Agent 的核心难点:模型输出的不确定性。它可能不调工具直接回答错误答案,可能连续调用工具十几次,可能把参数拼错。可靠系统要解决的就是这些问题。
2. 从最小循环到可用系统:绕不开的准备
最小循环只是骨架,真实项目里还要处理工具定义、记忆、规划等基础设施。这一节讲三件绕不开的事。
2.1 工具调用与函数定义的边界
工具定义是把外部能力暴露给模型的关键。很多新手把函数写得很大,结果模型经常用错。我踩过几次坑之后总结出几个原则:
- 一个工具只做一件事,参数要少且语义清晰。比如不要写一个“全能数据查询器”,而是拆成 get_user_info、get_order_list。
- description 要写清楚“什么时候用”,最好带上“如果用户没提订单,不要调用订单查询”这种约束。
- 参数类型要严格,能枚举就枚举,模型填错的概率会降低很多。
- 工具返回值要简洁,只返回必要信息,大段日志直接扔进上下文会浪费 token,还会干扰模型判断。
工具定义还涉及权限边界。Agent 能调用的工具就是它的“上限”。如果你的 Agent 需要访问内部系统,一定要做细粒度的权限控制,比用户命令能触达的工具列表更小。不要把所有数据库读写权限直接暴露给模型,这跟给员工发一张无限额度的公司卡没区别。
2.2 记忆与上下文管理
最小循环里所有历史消息都会进入上下文,但真实业务场景下对话不会只有三轮。我见过一个客服 Agent,聊到第十轮,系统提示词加完整的商品列表加历史记录,一次请求直接烧掉 3 万 token,响应也变慢。
上下文管理的常见手段有三种:
- 截断:只保留最近 N 轮对话,并且手动压缩系统提示词。
- 摘要:每几轮对话后,让模型把前面的内容总结成一段摘要,塞回上下文。
- 向量检索:把长期信息存入向量数据库,需要时用相似度检索出相关片段,再注入上下文。
这三种各有优劣。截断最省事,但会丢失关键信息;摘要能保住重点,但摘要本身可能出错;向量检索适合知识库问答,但实现复杂度高。我通常的做法是组合使用:核心上下文始终保留,中间轮次做摘要,外部知识通过 RAG 检索动态加入。
还要注意多轮对话里的“状态”。Agent 的工具调用完成后,如果后续决策还依赖之前的中间结果,一定要把关键结果显式写入上下文,而不是指望模型自己记住。模型对细节的记忆能力远没有想象中那么强。
2.3 规划与任务拆解的工程化
很多人把“规划”理解成让模型写一个 todo list 再逐项执行。这在简单任务里可行,但在复杂场景里,单个模型输出的 todo list 往往质量不稳定。
更工程化的做法是让“规划”显式化。有两种主流思路:
- ReAct 风格:不强制显式规划,让模型在循环中动态决定下一步。
- Plan-and-Execute 风格:先让模型拆解出任务清单,然后逐个执行,执行完再检查是否符合目标,不符合就重新规划。
Plan-and-Execute 看起来高级,但实际落地要小心。如果任务清单本身拆错了,会出现做了半天无用功的情况。我一般会加一道“规划校验”的步骤,让模型先告诉自己“这个任务的关键依赖是什么,哪些可以并行,哪些必须串行”,再进行工具调度。
另外,任务拆解要区分“动态拆解”和“静态模板”。很多业务场景其实有固定流程(比如订单售后流程:确认身份 → 查单 → 判断责任 → 给出方案),这种场景直接用状态机,比让模型自由发挥可靠得多。Agent 的自由度应该被约束在实际需要的范围内。
3. 把 Agent 做成可靠系统:工程实践才是分水岭
最小循环像赛车引擎,可以直接上赛道,但不能保证不爆缸。可靠系统要做的是给引擎加冷却、加安全笼。这一节我从五个工程维度拆解。
3.1 超时、重试与失败处理
大模型调用不是本地函数,它可能因为网络问题、服务端过载而返回错误,也可能一直挂在那里。不设超时的生产系统就是定时炸弹。
我给所有外部调用设三级超时:连接超时、读取超时、整体请求超时。以 OpenAI SDK 为例:
client = OpenAI( timeout=httpx.Timeout(connect=5.0, read=30.0, write=10.0, pool=5.0) )除了网络超时,还有工具执行本身的超时。比如一个工具要调用第三方 API,对方挂了,整个 Agent 就会被卡住。工具执行建议用asyncio.wait_for或线程池的Future设置超时,超时后给模型返回“工具执行超时,请稍后重试”,而不是让循环挂死。
重试也不是盲目重试。模型调用失败要区分是限流还是服务端错误。限流要等待后重试;参数错误重试一万次也没用。我常用的策略是:5xx 和连接错误做指数退避重试,4xx 直接抛出并终止当前请求。
失败处理的关键是做兜底话术。当所有重试都失败时,Agent 不应该硬着头皮编答案,而应明确告诉用户“我现在无法获取天气信息,请稍后再试”。这会让产品体验有很大提升。
3.2 并发与资源控制:Agent 怎么扛并发
“ai agent 怎么扛并发”是被问最多的一个问题。这里先说结论:Agent 的并发瓶颈通常不在代码,而在模型 API 的速率限制和上下文大小。你把 Agent 内部循环拆得再细,只要每个请求还是同步阻塞地调模型,并发一高必然完蛋。
扛并发要从几层入手:
- 异步化:Agent 的循环可以使用
asyncio改造,让模型调用和工具调用都变成异步非阻塞。FastAPI 天生支持异步,配合AsyncOpenAI客户端可以很好地服务高并发请求。 - 并发上限控制:不要无限制地创建协程,要使用信号量或连接池限制同时进行的 Agent 任务数。我之前线上出现过几百个并发请求同时打到模型 API 导致限流的情况,加了
asyncio.Semaphore(20)后稳定很多。 - 请求合并:有些模型 API 支持 batch,可以把多个短请求合并成一个批量请求。但不是所有场景都适用,因为 Agent 循环依赖上一步的结果,只有真正独立的请求才能合并。
- 状态隔离:每个 Agent 会话的状态要独立存储,不能放在全局变量里。用 Redis 按 session_id 存储上下文,多个 worker 实例才能水平扩展。
还有一点容易被忽略:工具执行的并发度。Agent 里可能同时有多个工具返回,如果你的工具函数是阻塞的(比如同步的数据库查询),会挡住事件循环。高阶做法是把工具注册成异步函数,或者丢给线程池执行。
3.3 可观测性与日志体系
Agent 系统比普通 Web 接口难排查,因为内部的决策链路不可见。模型为什么这么回答、为什么调用了这个工具、工具返回了什么,不做追踪根本无从谈起。
可观测性要做到三个层面:
- 日志:按 session_id 和 trace_id 记录每一轮的输入输出。重点记录四件事:模型请求的消息数量、模型返回的内容或工具调用、工具执行结果、循环次数。
- 指标:统计任务成功率、平均完成时间、平均循环次数、token 消耗量、工具调用失败率。这些数据能直接反映 Agent 质量。
- 链路追踪:用 OpenTelemetry 的 Span 把一次 Agent 任务的所有环节串联起来。推荐把“规划→工具调用→模型再决策”的整体作为一个 trace,每个内部调用作为一个 span。
我还会专门给 Agent 加一个“思考日志”:把模型每次返回的原始响应(包括 tool_calls 完整 JSON)落盘,方便出问题时复现。很多人只看最终答案,但 Agent 的问题往往出现在中间步骤。
3.4 状态持久化与恢复
扛住宕机是可靠系统的底线。Agent 任务执行到一半,服务重启了怎么办?上下文还在吗?很多开发者会在 Redis 里存上下文列表,但 Redis 本身也可能丢数据。生产环境我会做两级存储:
- 实时状态:写 Redis,用于多实例访问和快速恢复。
- 审计备份:每完成一个关键节点(比如一次工具调用),把上下文同步写一份到数据库或对象存储。
恢复流程是:任务中断后,从备份恢复上下文,重新创建 LLM 客户端,从上次最后一个未完成的动作继续执行。这里有一个坑:恢复到“未调用工具”的状态还是“已调用但未观察结果”的状态?我建议恢复到“已调用但未观察结果”,避免工具副作用重复执行。比如“发送邮件”这种非幂等操作,重复执行会出大问题。
所以工具设计时就要考虑幂等性,或者至少让 Agent 记录执行结果。状态持久化不只是技术问题,更是业务安全边界问题。
4. 技术选型:框架不是银弹,取舍是关键
工具链现在非常繁荣,有 LangChain/LangGraph、Spring AI、Rust 方案,也有扣子这类低代码平台。选型本质是在评估风险:你对生态的依赖程度有多高?
4.1 LangGraph / LangChain / FastAPI 怎么配合
LangChain 把场景抽象得很全面,但正因为抽象太多,踩坑成本也高。LangGraph 解决了“有状态图”的问题,适合做复杂多跳 Agent,节点之间的状态管理比较清晰。FastAPI 是服务层的好选择,异步支持好,类型提示完整,写起接口来非常舒服。
我目前的实践组合是:业务服务用 FastAPI 提供 HTTP 接口,Agent 内部用 LangGraph 构建有状态的工作流,工具层直接用普通 Python 函数注册。LangGraph 的节点可以定义条件边,这种表达力很适合 Agent 循环。但如果你只需要一个简单的“调工具→再决策”循环,用 LangGraph 反而重了。
4.2 Spring AI 与 Java 生态
企业级 Java 团队往往绕不开 Spring AI。它是把 AI 能力注入 Spring 生态的一套框架,好处是能够复用现有 Spring Boot 的依赖注入、事务管理、监控体系。如果你的核心业务在 Java 里,用 Spring AI 能够减少语言切换的鸿沟。
但是 Spring AI 的迭代速度很快,API 变动比较频繁,社区沉淀也还不够。我接触下来,适合“在 Java 服务里嵌入一个 Agent 模块”的场景,但如果是做积木式的复杂 Agent 产品,Java 生态的灵活度会低一些。用之前先评估你的团队是否真的需要 Java 生态的稳定性,而不是因为“公司用 Java”就直接选。
4.3 Rust 的语言级优势
有人问“基于 Rust 语言做 AI Agent”,这个问题更多是冲着并发和性能去的。Rust 的内存安全和高并发能力确实让 Agent 服务可以承载更高密度的并发,同时保持极低的资源占用。理论上,一千个 Agent 任务的并发,Rust 的资源和稳定性都要比 Python 好。
但实际的痛点在于生态和迭代速度。Rust 的 LLM 框架还在早期,工具链不够丰富,上手门槛高,写业务代码效率低。我的建议是:除非你的 Agent 服务已经到了需要极致压榨并发性能的阶段,否则没必要用 Rust 从零写。比较务实的路线是主服务用 Python/Java,把高频或计算密集的子模块用 Rust 做成 sidecar 或独立服务。
4.4 低代码平台:扣子 Coze 这类工具能干嘛
扣子这类低代码平台确实能快速搭出 demo,很多人都拿它做一些小红书自动发消息、简单客服问答之类的 Agent。它的优势是内置了知识库、插件、工作流编排,不需要写代码就能做出一套可用的 Agent。
但低代码平台的边界也很明显:复杂状态管理、私有化部署、高并发定制化逻辑很难施展。我之前见过团队用扣子搭了一个业务流,后期想接入自己的鉴权和数据库发现非常别扭。它适合做原型验证、快速给非技术同事做自动化工具,真正上生产系统还是得回到代码。用的时候心里要清楚:低代码是“能做”,不代表“能所有场景都做得好”。
5. 可靠系统的评估:没有度量就没有优化
Agent 系统的质量是不能靠感觉判断的。模型换一个版本,可能对话质量提升但工具调用准确率下降,不跑评估根本发现不了。
5.1 离线评估集怎么建
准备一批覆盖典型场景的测试用例,每个用例包含:用户输入、期望走的步骤、期望的最终答案(或答案应该包含的关键信息)。数量不用太多,50-200 条就能发现大部分问题。
评估指标分三类:
- 任务成功率:Agent 是否正确完成了目标。
- 工具调用准确率:该调用哪个工具就调用哪个工具,不能乱调。
- 效率指标:平均循环次数、平均 token 消耗。
跑评估时可以用一个“评估模型”当裁判,让 gpt-4o 或本地模型打分。但裁判模型也有偏差,所以最好同时保留人工抽检。
5.2 线上回归与 A/B
离线评估只能守住下限,上线后还要做 real traffic 监控。最有效的办法是灰度:先让 5% 流量走新 Agent 版本,对比旧版本的任务完成率和用户满意度。每次改 prompt、换模型、调整工具参数,都要跑一轮灰度回归。
我还有一个习惯:把线上失败的样本自动收集到“bad case 库”,每周挑几个典型的更新到离线评估集里。这样评估集会越来越贴合真实场景,不会变成自嗨的玩具。
5.3 人在回路中的兜底
没有任何 Agent 能保证 100% 正确,所以可靠系统必须考虑人介入的机制。当 Agent 的自信心低或连续两次工具调用失败时,应该主动转人工。最低成本的做法是设置一个“bump 条件”:循环超过 N 次就停止自动执行,把当前上下文和候选方案一并交给人工客服处理。
人在回路不只是兜底,它也是收集反馈的机制。人工客服看到 Agent 的中间日志后修正一次,这个修正结果可以成为优化工具调用时效的训练数据。这条飞轮转起来,Agent 的质量才会持续提升。
6. 高频问题与实战排查
6.1 Agent 陷入了死循环
这是最常见的问题。模型在“调用工具→查看结果→再调用工具”之间反复横跳,可能因为上下文信息不足,也可能因为模型本身的毛病。排查思路:
- 先在循环里打点,记录每次工具调用的 name 和参数。看是不是同一组工具反复调用。
- 检查工具结果是否有用。很多死循环是因为工具返回信息不够,模型拿不到答案只能一直重试。
- 设置最大循环次数(比如 5 次),达到后让模型总结当前已获得的成果并给用户一个“当前信息不足”的回答。
你可能会看到“Agent 只调用一次工具就结束了”这种反向问题。排查下来往往是工具定义写得不好,模型认为一次调用就够了,或者系统提示词里缺少“你可以多次调用工具直到完成任务”的引导。这种需要反复调整 prompt,同时看评估集改进是否有效。
6.2 工具调用结果解析失败
模型返回的 tool_calls.arguments 是 JSON 字符串,但偶尔会有非法 JSON,比如多了一个逗号、键没带双引号。不要慌,最直接的处理是:捕获解析异常,把错误信息发给模型,让它重新生成参数。这相当于一次自动修复:
try: args = json.loads(tool_call.function.arguments) except json.JSONDecodeError: messages.append({ "role": "user", "content": f"你刚才的工具参数 {tool_call.function.arguments} 不是合法 JSON,请重新生成" }) continue另外,要保证工具函数的入参校验。模型可能传了 schema 里没有的字段,或者字段类型不对。不要直接抛异常,把异常信息包装成“工具执行失败,失败原因是 XXX,请检查参数”返回给模型,它通常能自我纠错。
6.3 并发一高就嗝屁
如果你已经用了异步但并发还是起不来,重点排查两件事:一是工具函数有没有用阻塞式同步 I/O;二是模型 API 的限流参数。很多云厂商的并发限制是每个 key 的 QPM(每分钟请求数),而不是并发数。异步跑起来后,可能瞬间打满配额。
正确的做法是引入令牌桶或简单的滑动窗口限流。如果用一个共享的 Redis 计数器控制“当前正在跑的 Agent 任务数”,超过阈值就直接返回 429 让客户端稍后重试。不要为了并发把所有请求都怼出去。
6.4 模型输出不稳定
同一个 prompt,模型这次答对那次答错,这很正常。应对思路:把 temperature 调到 0 或 0.1(在实际项目中,我用 0 配合重试,效果挺明显);在系统提示词里加入“必须遵守步骤”的显式约束;为关键场景准备 few-shot 示例,让模型模仿示例的输出格式。
更进阶的手段是用“结构化输出”功能,强制模型返回符合 JSON Schema 的结果。无论是工具调用的参数,还是最终回复,都限制成固定结构,能很大程度避免乱写的现象。
写在最后:我的一点实操体会
做 Agent 这一年多,我踩过最大的坑,不是模型能力不够,而是系统设计时把“可能性”当成了“确定性”。最小循环跑通只是第一步,后面每一步——超时怎么兜、并发怎么控、状态怎么存、坏案例怎么回收——才是真正决定 Agent 能不能上生产的因素。
另外我建议所有读者都手写一次最小循环,哪怕是几行代码。当你亲手看到模型返回的 tool_calls、亲手把工具结果塞回上下文、亲手调试死循环,你会对 Agent 的整个机理有非常直观的理解。之后再上 LangGraph、Spring AI 这些框架,你会发现它们只是帮你把循环的某几个环节固化下来,核心逻辑和你手写的那几行一样。
最后分享一个小技巧:Agent 遇到反复纠缠不清的问题时,不一定要继续在循环里打转,先给用户一个过渡性回答,把上下文记录下来,异步继续处理或转人工。业务上,让用户等三秒拿到一个“处理中”的状态,远好过让用户盯着 AI 转圈三十秒后得到一个错误结果。