尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
AI Agent开发避坑指南:ReAct循环、Function Calling与MCP工具接入实战
1. 半年迭代三次之后我才看清这五个“假性解决”的坑去年下半年到今年年初我带着一个小团队连续做了三个 AI Agent 应用从最开始的对话式助手到后面接入工具调用的任务型 Agent再到带记忆和规划能力的多步执行系统。中间经历过一次大重构两次推倒重来踩的坑多到可以写一本小册子。但真正让我难受的不是那些一眼就能看出来的 bug而是那些“看起来已经解决了”的问题——测试通过、演示流畅、上线当天也没炸结果跑了一两周之后各种诡异现象开始冒出来。这篇文章不讲概念科普也不讲“AI Agent 是什么”这种入门内容。我想聊的是五个具体的坑它们都有一个共同特征你在开发阶段会觉得自己已经处理好了但到了真实场景里问题会以完全不同的面貌重新出现。这五个坑分别涉及 ReAct 循环的终止判断、Function Calling 的参数校验、MCP 工具接入的边界、LangChain4j 的记忆管理以及并发场景下的状态污染。每一个我都会讲清楚当时怎么想的、怎么做的、为什么觉得解决了、后来怎么发现没解决、最后怎么真正解决的。如果你正在用 LangChain4j、Spring AI 或者自己手写 ReAct Agent或者正在接入 MCP 协议做工具编排这篇文章里的经验应该能帮你省掉至少两到三周的排查时间。如果你刚开始搭建 AI Agent也可以把这些坑当成设计阶段的检查清单提前避开。2. ReAct 循环的终止条件为什么“最大轮次”根本不够用2.1 最初的设计设一个 maxIterations 就以为万事大吉刚开始写 ReAct Agent 的时候我的逻辑很直接让模型在“思考—行动—观察”的循环里跑设一个最大轮次比如 10 轮超过就强制停止并返回当前结果。这个方案在 demo 阶段表现很好因为 demo 里的任务通常两三步就结束了根本碰不到上限。但上线之后我遇到了几种情况。第一种是模型在某个步骤上反复调用同一个工具每次返回的结果略有不同它就继续调直到把 10 轮耗尽。第二种是模型在“思考”阶段陷入了自我怀疑不断生成“让我再确认一下”之类的文本但迟迟不输出最终答案。第三种更隐蔽模型确实在 10 轮内给出了答案但这个答案是它在第 3 轮就应该给出的后面 7 轮全是在做无效的重复验证。这三种情况的共同点是maxIterations 这个硬性上限只能保证循环一定会停但完全不能保证循环停得合理。它就像一个只管关门不管质量的保安时间到了就赶人至于里面的人有没有办完事它不关心。2.2 真正的问题缺少“进展检测”机制后来我意识到ReAct 循环的核心问题不是“什么时候停”而是“怎么判断当前这轮有没有产生新信息”。如果连续两轮的行动和观察结果高度相似那就说明 Agent 在原地打转继续跑下去只是浪费 token 和时间。我最终的方案是引入一个简单的进展评分机制。具体做法是每一轮结束后把当前轮的“行动观察”与上一轮做对比计算一个相似度分数。如果相似度超过阈值我用的 0.85就标记为“无进展轮次”。连续两轮无进展就强制终止并把当前已收集到的信息整理成最终回答。这个相似度计算不需要很复杂我用的是简单的文本重叠率加上关键实体匹配。比如上一轮调用了“查询订单”工具返回了订单号 A123这一轮又调用了同一个工具返回的订单号还是 A123那基本可以判定为重复。如果返回的是 A124那说明有进展继续跑。注意这个阈值不要设得太低否则正常的重试也会被误判为无进展。我试过 0.7结果模型在正常的多步推理中被频繁打断。0.85 到 0.9 之间是比较稳妥的范围。2.3 另一个隐蔽问题模型自己“假装”给出了最终答案还有一种情况让我头疼了很久。模型在第 5 轮的时候输出了一个看起来像最终答案的文本格式也对内容也像那么回事。但如果你仔细看会发现它其实还没有拿到关键数据只是根据已有信息“编”了一个合理的回答。这种情况在 ReAct 框架里特别容易发生因为模型被训练成倾向于给出完整回答而不是说“我还没查完”。我的应对方式是在系统提示词里明确要求只有在所有必要信息都已通过工具调用获取之后才能输出最终答案。同时在解析模型输出时增加一个校验步骤如果模型输出了最终答案但当前轮次还没有任何工具调用记录或者关键字段缺失就拒绝这个答案强制模型继续执行。这个校验逻辑听起来简单但实际写起来需要你对业务字段有清晰的定义。比如一个订单查询 Agent关键字段就是订单号、状态、金额。如果模型给出的答案里缺少这三个中的任何一个就不算完成。2.4 实测下来的效果和遗留问题引入进展检测和答案校验之后无效循环的比例从原来的 30% 左右降到了 5% 以下。Token 消耗也明显下降因为大部分无效轮次被提前终止了。但也不是没有代价。进展检测本身需要额外的计算虽然不重但在高并发场景下会累积成可观的延迟。另外有些任务确实需要多轮重复调用同一个工具比如轮询某个状态直到它变化。这种情况下进展检测会误判。我的解决办法是允许在工具定义里标记“可轮询”属性对于这类工具进展检测的阈值放宽或者暂时关闭。3. Function Calling 的参数校验模型给的参数永远比你想的更“灵活”3.1 你以为定义了 schema 就安全了用 Function Calling 的时候大多数人都会定义 JSON Schema声明每个参数的类型、必填项、枚举值。然后你就觉得模型会老老实实按 schema 来。我一开始也是这么想的直到我发现模型会给出这些玩意儿日期格式写成 “2024年1月1日” 而不是 “2024-01-01”枚举值明明定义了三个选项它给你返回第四个必填参数倒是给了但值是空字符串数字类型的参数它给你返回字符串 “123”这些问题在开发阶段很难发现因为你的测试用例通常是你自己写的模型在那些用例上表现正常。但真实用户的输入千奇百怪模型在解析用户意图时会产生各种偏差。3.2 三层校验schema 校验只是第一层我后来把参数校验分成了三层。第一层是 JSON Schema 校验用标准的校验库比如 Java 里的 networknt/json-schema-validator做类型和格式检查。这一层能拦住大部分明显的格式错误。第二层是业务规则校验。比如日期不能是过去的时间、金额不能为负数、枚举值必须在业务允许的范围内。这一层需要你自己写代码因为 schema 表达不了这些约束。第三层是语义校验。这一层最容易被忽略但也最重要。比如模型调用“发送邮件”工具参数里收件人、主题、正文都有格式也对但收件人地址是一个不存在的域名。这种问题 schema 和业务规则都拦不住只能在实际执行前做一次轻量级的语义检查比如域名是否存在、用户是否有权限等。// 以 LangChain4j 为例工具方法内部做二次校验 public String sendEmail(String to, String subject, String body) { if (!isValidEmailDomain(to)) { return 错误收件人域名无效请确认后重试; } if (subject null || subject.trim().isEmpty()) { return 错误邮件主题不能为空; } // 实际发送逻辑 return emailService.send(to, subject, body); }提示工具方法返回错误信息时不要抛异常而是返回一个结构化的错误描述。这样模型能理解发生了什么并在下一轮尝试修正。抛异常会导致整个 Agent 流程中断模型没有机会自我修复。3.3 参数修正让模型自己改比你自己改更靠谱当校验失败时有两种处理方式。一种是你在代码里自动修正比如把 “2024年1月1日” 转成 “2024-01-01”。另一种是把错误信息返回给模型让它重新生成参数。我两种都用过实测下来对于格式类问题自动修正效率更高对于语义类问题让模型自己改更靠谱。因为格式转换是确定性的你写一次代码就能覆盖所有类似情况。但语义问题往往需要结合上下文模型比你更清楚用户到底想要什么。比如用户说“帮我订明天下午的会议室”模型第一次调用时把日期算错了。你把错误信息返回给它它看到“日期不能是过去时间”之后通常能自己修正。但如果你在代码里硬编码一个日期修正逻辑反而可能改错。3.4 一个容易被忽略的细节参数默认值有些工具的参数是可选的模型可能不传。这时候你的代码需要有合理的默认值。但默认值不能随便设否则会导致意料之外的行为。比如一个“查询订单”工具如果不传订单号默认查最近一条那模型可能会在用户想查特定订单时因为没传订单号而返回了错误的订单。我的做法是对于关键参数即使 schema 里标记为可选在业务逻辑里也当作必填处理如果模型没传就返回错误让它重新生成。对于真正的可选参数默认值要尽可能保守比如分页查询的默认页大小设为 10 而不是 100避免一次拉取过多数据。4. MCP 工具接入协议通了不代表工具就能用4.1 MCP 解决的是“怎么连”不是“连上之后怎么用”MCP 协议最近很火它确实解决了工具接入的标准化问题。以前每接一个工具都要写一套适配代码现在只要工具实现了 MCP Server理论上就能直接接入。但我在实际接入过程中发现协议层面的连通只是第一步真正麻烦的是工具描述的质量和调用边界的界定。我接的第一个 MCP 工具是一个浏览器自动化工具。协议握手成功工具列表也能正常获取但模型在调用时频繁出错。原因是工具的描述写得太笼统比如“在页面上执行操作”这种描述模型根本不知道什么时候该用、什么时候不该用。后来我把描述改成了“当需要点击页面上的按钮或链接时使用参数包括选择器和等待时间”调用成功率立刻上去了。4.2 工具粒度的取舍太粗和太细都是坑MCP 工具的设计有一个粒度问题。一个工具如果功能太粗比如“处理用户请求”那模型根本不知道怎么传参。如果功能太细比如“在输入框输入第一个字符”“在输入框输入第二个字符”那模型会陷入无穷无尽的工具调用。我摸索出来的原则是一个工具应该对应一个完整的、有业务意义的操作。比如“填写登录表单”是一个合适的粒度它内部可以包含多个步骤但对模型来说就是一个原子操作。而“点击登录按钮”就太细了因为模型还需要知道怎么填表单。注意MCP 工具的命名也很关键。用动词开头比如 “query_order”、“send_notification”、“create_ticket”比用名词开头更容易让模型理解。4.3 工具返回值的格式结构化比自然语言好MCP 工具的返回值格式直接影响模型的后续决策。我一开始让工具返回自然语言描述比如“查询成功订单状态是已发货”。后来发现模型在解析这种返回值时经常抓错重点。改成结构化 JSON 之后模型的理解准确率明显提升。{ status: success, data: { order_id: A123, order_status: shipped, estimated_delivery: 2024-01-05 } }但结构化也有代价就是 token 消耗会增加。我的折中方案是对于简单查询返回精简的结构化数据对于复杂结果返回结构化数据加上一句自然语言摘要。这样模型既能准确解析又能快速理解。4.4 MCP 工具的并发调用别让一个慢工具拖垮整个 AgentMCP 工具调用通常是同步的如果某个工具响应很慢整个 Agent 流程就会被阻塞。我在接入一个外部 API 工具时遇到了这个问题那个 API 平均响应时间 3 秒高峰期能到 10 秒。模型在等待期间什么也做不了。后来我做了两件事。一是给每个工具调用设置超时超过 5 秒就返回超时错误让模型决定是重试还是换方案。二是对于可以并行的工具调用改成异步执行。比如模型需要同时查询订单状态和物流信息这两个调用没有依赖关系可以并行发起。但并行调用也有风险就是模型可能不理解为什么两个结果一起返回了。我的做法是在返回结果里明确标注每个结果的来源比如 “order_status_result” 和 “logistics_result”这样模型能对应上。5. LangChain4j 的记忆管理短期记忆不是越多越好5.1 默认的记忆策略在真实场景下会爆LangChain4j 提供了多种记忆策略我一开始用的是最简单的“保留最近 N 条消息”。设了 N20觉得应该够用。结果在一个多轮对话场景里用户和 Agent 聊了 30 多轮之后Agent 开始忘记前面说过的关键信息。原因是 20 条消息里包含了大量的工具调用记录和中间结果真正有用的对话内容被挤掉了。后来我改成了按 token 数量限制而不是按消息条数。因为工具调用的返回结果可能很长一条消息就能占几百个 token。按 token 限制更合理我设的是 4000 token 的短期记忆窗口。5.2 工具调用记录要不要放进记忆这是一个很容易忽略的问题。工具调用的请求和返回结果要不要作为记忆的一部分我试过两种方案。方案一全部保留。好处是模型能看到完整的执行历史方便它做后续决策。坏处是 token 消耗大而且很多工具返回结果对后续对话没有价值。方案二只保留工具调用的摘要。比如“查询了订单 A123状态是已发货”而不是完整的 JSON 返回。好处是节省 token坏处是模型可能丢失一些细节。我最终采用的是混合方案对于最近两轮的工具调用保留完整结果对于更早的调用只保留摘要。这样既保证了近期决策的准确性又控制了 token 消耗。5.3 长期记忆的写入时机别等对话结束才存长期记忆通常是在对话结束后写入向量数据库下次对话时检索。但我在实践中发现等对话结束再存有两个问题。一是如果对话很长结束时的信息可能已经过时了。二是如果对话意外中断这次对话的记忆就丢了。我改成了在对话过程中实时写入关键信息。具体做法是每当模型输出一个包含关键事实的句子比如用户说“我的订单号是 A123”就立刻把这条信息写入长期记忆。这样即使对话中断关键信息也不会丢。但实时写入也有代价就是写入频率高向量数据库的压力大。我的优化是加一个简单的去重逻辑如果新信息和已有记忆的相似度超过 0.95就跳过写入。5.4 记忆检索的精度问题相似不等于相关长期记忆检索通常用向量相似度但相似度高的记忆不一定对当前对话有用。我遇到过好几次检索出来的记忆和当前话题在语义上很接近但实际上是不同用户、不同订单的信息。解决这个问题的关键是给记忆加上元数据过滤。比如每条记忆都带上用户 ID、会话 ID、时间戳。检索时先按元数据过滤再做向量相似度排序。这样能大幅减少不相关的记忆被召回。// 伪代码示例带元数据过滤的记忆检索 ListMemory memories memoryStore.search( queryEmbedding, filter(user_id, userId), filter(timestamp, , thirtyDaysAgo), limit(5) );6. 并发场景下的状态污染单用户测试永远发现不了的问题6.1 一个让我排查了两天的诡异 bug上线两周后有用户反馈说 Agent 有时候会“答非所问”比如问的是订单 A 的状态返回的却是订单 B 的信息。我一开始以为是模型幻觉后来发现不是。同一个会话里连续两次查询不同订单第二次的结果里混入了第一次的订单号。排查了两天才找到原因我在 Agent 的上下文对象里缓存了工具调用的结果但没有做会话隔离。当两个请求并发执行时它们共享了同一个上下文对象导致数据串了。这个问题在单用户测试时完全发现不了因为单用户不会并发。只有多个用户同时使用时才会触发。6.2 状态隔离的三个层次后来我把状态隔离分成了三个层次。第一层是会话级隔离每个会话有独立的上下文对象用会话 ID 做 key。第二层是请求级隔离同一个会话内的不同请求如果并发执行也要有独立的临时状态。第三层是工具级隔离工具内部的缓存和状态也要做隔离不能跨请求共享。第一层和第二层相对好做用 ThreadLocal 或者 Reactor 的 Context 就能解决。第三层比较麻烦因为有些工具本身是有状态的比如一个浏览器自动化工具它维护了一个浏览器实例。如果多个请求同时操作同一个浏览器实例就会互相干扰。我的解决方案是给每个会话分配独立的工具实例会话结束后再回收。这样虽然资源消耗大一些但避免了状态污染。6.3 并发下的模型调用限流和降级除了状态污染并发还会带来另一个问题模型 API 的限流。当并发请求数超过模型服务的配额时请求会被拒绝或者排队。我一开始没有做限流结果高峰期大量请求失败。后来我加了一个简单的令牌桶限流器每个用户每秒最多发起 2 个 Agent 请求。超过的请求进入队列等待或者直接返回“当前请求较多请稍后重试”。同时对于非核心功能比如记忆检索在高峰期可以降级为跳过优先保证主流程可用。提示限流阈值不要设得太死要根据实际模型服务的配额来调整。我一开始设得太低导致正常用户也被限流了。后来改成动态调整根据模型服务的响应时间自动升降阈值。6.4 压测怎么做才有效并发问题不能靠猜必须压测。我用的工具是 JMeter 加上自定义的 Java 采样器模拟多个用户同时发起不同类型的 Agent 请求。压测时重点关注三个指标响应时间、错误率、以及状态一致性。状态一致性怎么测我的做法是在压测请求里埋入唯一的标记比如每个请求带一个唯一的订单号然后检查返回结果里的订单号是否和请求一致。如果不一致就说明有状态污染。第一次压测就发现了问题并发 50 的时候状态污染率大概 3%。修复之后并发 200 的情况下状态污染率为 0。这个测试让我对系统的并发能力有了真实的认知而不是靠感觉。7. 这半年下来我对“看起来解决了”这五个字的理解写完这五个坑我发现它们有一个共同模式都是在开发阶段用简化场景验证通过但在真实场景的复杂性面前暴露出来的。ReAct 循环在简单任务上跑得很好但遇到重复调用就失控Function Calling 在标准参数上没问题但模型总会给你惊喜MCP 工具接通了不代表能用好记忆管理在短对话里看不出问题并发状态污染在单用户测试中根本不会出现。我现在做 AI Agent 开发会刻意问自己一个问题这个方案在什么情况下会失效如果我想不出来那说明我还没有真正理解它。另一个习惯是任何在 demo 阶段“看起来没问题”的东西我都会在真实数据上跑至少一周再下结论。因为 AI Agent 的问题往往不是逻辑错误而是概率性的、边界性的只有足够多的样本才能暴露出来。如果你也在做类似的事情我的建议是不要相信第一次测试通过的结果不要相信单用户场景的表现不要相信模型在标准输入下的行为。多问几个“如果”多跑几轮真实数据很多坑其实是可以提前发现的。
RELATED

相关推荐

Vue3 Pinia持久化中indexOf对象查找问题解析

Vue3 Pinia持久化中indexOf对象查找问题解析

1. 问题背景与现象描述最近在重构一个Vue3项目时,我遇到了一个相当隐蔽的Bug:当使用indexOf方法查找Pinia存储中的对象,并且启用了持久化功能时,出现了数据匹配异常的情况。具体表现为:明明两个对象内容完全一致&#…

📅 2026/11/2 8:53:24
BBR-A算法优化:解决BBR拥塞控制的公平性问题

BBR-A算法优化:解决BBR拥塞控制的公平性问题

1. BBR拥塞控制算法公平性问题解析在2016年Google提出的BBR(Bottleneck Bandwidth and Round-trip propagation time)拥塞控制算法,通过测量网络路径的瓶颈带宽和最小往返时延(RTT)来调整发送行为,相比传统…

📅 2026/11/2 8:53:24
深入解析代理模式:静态与动态代理实现原理

深入解析代理模式:静态与动态代理实现原理

1. 代理模式的核心价值与应用场景代理模式(Proxy Pattern)是结构型设计模式中最常用的模式之一,它的核心价值在于:在不修改原始类代码的前提下,通过引入代理对象来控制对原始对象的访问。这种控制可以表现为多种形式&a…

📅 2026/11/2 8:53:24
MORE NEWS

更多资讯

📰

团队规模决定工具选型:编程效率提升工具的匹配之道

前阵子一个技术负责人找我来聊,说他们团队大概二十来人,一年内陆续上了AI编程助手、在线代码评审、自动构建流水线、任务管理平台,每个工具单看都不差,但半年下来最直观的感受是“该加班还是加班,该延期还是延期”。他…

📰

AI治理、算力与模型能力三角共振:从三条新闻看行业新节奏

今天早上一睁眼,信息流里三条消息并排躺着:一条是国际治理层面关于AI"限速"的听证讨论,一条是云栖大会上的真武V900算力硬件亮相,一条是Gemini 4幽灵模型泄题的社区热议。这三件事单拎出来任何一件,都值得单…

📰

AI资讯日报选题逻辑与信息筛选框架:从信息过载到高质量降噪的工程实践

1. 一份AI资讯日报的选题逻辑与信息筛选框架每天早上打开十几个信息源,从论文预印本到厂商博客再到开发者社区的热帖,把真正值得看的内容挑出来、压缩成一份能在十分钟内读完的日报——这件事我从2023年就开始做了,中间踩过的坑比想象中多得多…

📰

大模型入门路线:从原理、微调到部署的完整指南

大模型这个领域,现在最不缺的就是"资料",缺的是"能把资料串成路线的人"。我经常收到私信问"我想入行大模型,该先看论文还是先学Python",也见过不少朋友把模型API文档当成入门教材,结果学…

📰

哈希表优化四数相加II问题解法详解

1. 问题背景与核心思路 四数相加II是算法中一个经典的哈希表应用场景。给定四个整数数组nums1、nums2、nums3和nums4,要求统计有多少个元组(i,j,k,l)满足nums1[i] nums2[j] nums3[k] nums4[l] 0。这个问题看似简单,但如果采用暴力解法,时…

📰

RFID技术在信创资产管理中的应用与创新

1. 项目概述:RFID如何终结条码时代的资产管理痛点十年前我第一次参与企业固定资产盘点时,亲眼目睹了行政部同事跪在地上用扫码枪逐个扫描设备条码的狼狈场景。当扫码枪第三次发出"滴——"的报警声时,那位同事崩溃地发现又是条码污损…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬