ReAct模式实现
ReAct模式实现
🧠 面试现场回答:ReAct 模式原理与 Java 实现
面试官您好,ReAct 是当前大模型 Agent 最主流的落地范式,我从核心原理、执行流程、Java 落地实现、实际踩坑这几个维度来展开说明。
1. 核心本质:边想边干,解决大模型 “瞎编” 痛点
ReAct 拆解为 Reasoning(推理) + Acting(行动),本质是模仿人类解决复杂问题的思维模式:
- 纯大模型对话相当于闭卷考试,全靠训练数据里的知识,容易出现幻觉、知识过时、无法操作外部系统的问题
- ReAct 模式相当于开卷考试:模型先分析问题、规划步骤,再调用外部工具(搜索、数据库、API 等)获取真实信息,拿到结果后再复盘推理,循环迭代直到解决问题
和思维链(CoT)的核心区别:CoT 只是纯文本脑补推理,ReAct 是思考 + 执行的完整闭环,能真正和外部世界交互。
2. 🔄 标准执行流程(三步一循环)
核心逻辑是 Thought → Action → Observation 三轮循环,直到模型判断问题可直接回答。
三个面试必提的核心要素:
- Thought:模型的推理过程(“内心 OS”),保证逻辑可解释、可追溯
- Action:要执行的具体动作,包含工具名称和调用参数
- Observation:工具执行后的真实返回结果,作为下一轮推理的输入依据
3. 🛠️ Java 技术栈落地实现
我之前项目里基于 Spring AI 做了封装,核心分为工具注册中心、Agent 执行引擎、上下文管理器三个模块。
技术选型
- 模型交互:Spring AI 对接主流大模型,也可直接用官方 HTTP SDK
- 工具管理:自定义工具接口 + Spring 自动扫描注册
- 会话上下文:单次请求用 ThreadLocal,多轮长会话存 Redis
核心代码骨架(面试讲清逻辑即可)
// 工具统一抽象接口
public interface AgentTool {
// 工具唯一标识,模型通过名称匹配
String toolName();
// 工具功能描述,写入Prompt告诉模型适用场景
String toolDesc();
// 工具执行逻辑
String run(String param);
}
// ReAct Agent 核心执行引擎
public class ReActAgent {
private final ChatModel chatModel;
private final ToolRegistry toolRegistry;
private static final int MAX_STEP = 5; // 最大步数兜底,防止死循环
public String chat(String userQuestion) {
List<Message> context = buildInitContext(userQuestion);
for (int i = 0; i < MAX_STEP; i++) {
// 1. 调用大模型,获取推理输出
String modelOutput = chatModel.call(context);
// 2. 解析输出是否包含工具调用指令
Optional<ActionCmd> actionOpt = parseAction(modelOutput);
if (actionOpt.isEmpty()) {
// 无工具调用,直接返回最终答案
return modelOutput;
}
// 3. 执行工具,拿到观测结果
ActionCmd cmd = actionOpt.get();
String observation = toolRegistry.execute(cmd.getToolName(), cmd.getParam());
// 4. 把本轮交互追加到上下文,进入下一轮推理
context.add(new AssistantMessage(modelOutput));
context.add(new UserMessage("Observation: " + observation));
}
return "抱歉,超出最大推理步数,无法完成任务";
}
}最关键的设计细节
- System Prompt 是核心:必须在系统提示词中强制输出格式,要求用
Thought:Action:Action Input:标记,并补充 1-2 个示例,模型才会按规范输出 - 解析容错:纯文本解析故障率高,生产环境优先用大模型原生的 Function Calling 能力,由框架自动解析参数
- 安全兜底:必须设置最大迭代步数,同时增加工具参数校验,防止高危接口被误调用
4. ⚠️ 常见踩坑与优化方案
| 问题场景 | 优化方案 |
|---|---|
| 模型输出格式混乱,解析失败率高 | 优先用原生 Function Calling;Prompt 增加 Few-Shot 示例;用 JSON Schema 约束输出结构 |
| 多轮后上下文膨胀,Token 成本飙升 | 仅保留最近 3 轮交互历史;对历史信息做摘要压缩;切换长上下文模型 |
| 重复调用同一工具,陷入死循环 | 检测重复 Action 主动打断;Prompt 要求每一步必须有新进展;最大步数强制兜底 |
| 工具参数错误,执行频繁报错 | 工具描述写清入参格式和约束;报错信息返回给模型自我修正;增加前置参数校验层 |
5. ✅ 适用场景总结
- 推荐使用:实时信息查询、多步骤运维操作、知识库问答、复杂任务规划等所有需要外部信息 + 多步推理的场景
- 不推荐使用:简单闲聊、常识类问题,用 ReAct 反而增加延迟和算力成本,属于过度设计
真实面试模拟
真实面试模拟
面试官 👨💼:
您好,欢迎。我看您简历里提到了对 AI Agent 和 ReAct 模式比较熟悉,那咱们就先聊聊这个。能先用一两句话概括一下,您理解的 ReAct 模式是什么吗?
候选人 💬:
好的面试官。ReAct 其实就是Reasoning + Acting,让大模型像人一样边推理边干活儿。它不会直接蹦出个答案,而是“推理出下一步要干什么 → 调用工具干活 → 看到结果再推理”,这么反复循环,直到能给出最终答案。相当于把人的思考链和外部的工具调用连起来了。
面试官 👨💼:
这个概括挺清晰的。那能不能具体描述一下这个“推理-行动-观察”的循环过程?最好能边画边讲。
候选人 💬:
没问题,我口头画个图您就明白了。过程就像这样:
用户问题
↓
[ 思考 Thought ] ← 大模型分析:“现在需要做啥?”
↓
[ 行动 Action ] ← 决定调工具,比如 search("北京天气")
↓
[ 观察 Observation ] ← 工具返回:“晴,28°C,轻度污染”
↓
得到最终答案了吗?
├─ 是 → 输出 Final Answer
└─ 否 → 带着 Observation 回到 Thought 继续思考实际运行中,这每一步的输出都会拼回上下文,模型看着整条“思维轨迹”来决定下一步。比如查完天气后发现污染,模型会推理“轻度污染,不适合哮喘患者跑步”,再追加查询用户健康情况,或者直接给建议。
面试官 👨💼:
那这个 ReAct 和咱们常说的思维链(CoT)相比,到底厉害在哪?它主要解决什么痛点?
候选人 💬:
我画个简单的对比您看:
| 普通思维链 (CoT) | ReAct 模式 | |
|---|---|---|
| 能不能调外部工具 | ❌ 不能 | ✅ 能调 API/数据库/搜索引擎 |
| 处理实时信息 | ❌ 只能靠训练数据编 | ✅ 实时获取,事实核查 |
| 幻觉控制 | 容易一本正经胡说八道 | 用真实数据大幅减少幻觉 |
| 落地复杂度 | 低 | 中等,需要解析器和工具框架 |
最大的痛点就是大模型“不懂装懂”。比如问“今天美元汇率”,CoT 可能会从训练记忆里猜一个过时的数字,ReAct 则会真实调汇率接口,拿回来的结果是确凿的。它现在是所有 AI Agent 的基础范式,像 AutoGPT、各种智能客服背后都是这套。
面试官 👨💼:
说得挺到位。那作为 Java 研发,如果让你在项目里落地 ReAct,具体代码结构怎么设计?可以聊聊关键伪代码。
候选人 💬:
核心就是个带工具注册中心的循环解析器。我边写边解释:
public class ReActAgent {
private final LLMClient llm; // 封装大模型调用
private final ToolRegistry tools; // 本地工具集合
public String run(String userQuestion) {
List<Message> context = new ArrayList<>();
context.add(new SystemMessage(REACT_PROMPT)); // 强制格式提示
context.add(new HumanMessage(userQuestion));
for (int step = 0; step < MAX_ITERATIONS; step++) {
String llmOutput = llm.chat(context); // ① 模型输出
Parsed parsed = parseOutput(llmOutput); // ② 解析动作
if (parsed.isFinalAnswer()) {
return parsed.getFinalAnswer(); // 🎯 结束
}
// ③ 执行工具,得到 Observation
String obs = tools.execute(parsed.getAction(),
parsed.getActionInput());
// ④ 把模型输出和观察结果都塞回上下文
context.add(new AssistantMessage(llmOutput));
context.add(new HumanMessage("Observation: " + obs));
}
return "抱歉,处理超时了";
}
}这里面的工程难点我总结一下:
- 解析稳健性 🧰:大模型偶尔格式错,得用正则兜底 + 失败重试时补充提示“请严格按格式输出”。
- 上下文膨胀 📈:每轮都堆积 Thought-Action-Observation,token 消耗飞快,必须限制最大步数,必要时做摘要截断。
- 工具异常处理 🔧:API 超时了不能直接崩,要把错误包装成
Observation: Error - timeout还给模型,它自己会重新推理。 - 框架化 🧩:现在 LangChain4j 已经原生支持 ReAct,用
@Tool注解业务方法,框架自动循环解析,省力不少。
面试官 👨💼:
很实际。那假如咱们现在要做一个智能客服 Agent,你会给它设计哪些工具?
候选人 💬:
一般我会分三类:
- 查询类 📋:订单查询、物流查询、用户画像查询,用于回答“我的快递到哪了”这类问题。
- 操作类 ⚙️:退款申请、修改收货地址,得做二次确认和安全校验,比如走审批流。
- 知识库类 📚:比如检索公司产品文档、FAQ,结合 RAG 来回答开放性问题。
每个工具注册进工具表,提示词里用自然语言描述清楚功能、参数,模型就能自动决策。比如用户说“我要退款”,模型推理出的 Action 就是 refund,Action Input 是订单号。
面试官 👨💼:
考虑得挺周全的。那在实现中,如果模型陷入死循环,比如反复查一个工具但拿不到想要的结果,你怎么防范?
候选人 💬:
两手抓:代码层面强行限制最大循环次数(比如 8 次),超了就熔断兜底;提示词里也要加引导语,比如“如果多次尝试仍无法解决,请直接给出部分结果并说明困难”。模型会学着在适当时候收手。
面试官 👨💼:
好的,今天关于 ReAct 我们就聊到这儿,看得出您对原理和落地都有实操,挺好的 👍。您有什么想问我的吗?
候选人 💬:
谢谢面试官!我想了解一下团队目前在 Agent 这块的技术栈偏向自研框架还是 LangChain4j 这类开源方案?😄
