前言
我是宋哥,6年Java后端,正在转型AI Agent开发。
Week1我从零搭了一个AI代码审查助手code-review-agent,5个版本迭代:Day1调通ChatAPI,Day2接入代码解析,Day3加上对话记忆,Day4实现ReAct推理,Day5才是完整版——ReAct + 自我反思 + 迭代约束。
今天花一整天消化这三个核心组件的架构和工作原理,写出来跟大家交流。
⚠️ 本文基于JDK 21 + Spring AI 1.1.1 & Spring AI Alibaba 1.1.0.0,实战验证过。
一、ChatMemory:分层架构下的对话记忆管理
1.1 为什么需要 ChatMemory?
Day3我遇到一个坑:直接调ChatAPI时,每次对话都是独立的,LLM根本不记得上下文。翻了Spring AI文档才发现,它提供了ChatMemory接口。
核心区别:
ChatMemory:有策略的上下文窗口,控制输入给LLM的消息范围(短期记忆)ChatHistory:完整的消息记录,用于长期归档(长期存储)
java
public interface ChatMemory {
void add(String conversationId, List<ChatMessage> messages);
List<ChatMessage> get(String conversationId, int capacityIndex);
void clear(String conversationId);
}
1.2 分层架构:策略层与持久化层分离
Spring AI做了很好的分层设计:
plaintext
┌─────────────────────────────────────────────┐
│ ChatMemory (策略层) │
│ MessageWindowChatMemory (滑动窗口) │
│ - retain(20) 保留最近20条消息 │
│ - 超出容量自动截断最旧消息 │
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ ChatMemoryRepository (持久化层) │
│ InMemoryChat... │ JdbcChatMemoryRepository │
│ - 开发/测试环境 │ - 生产环境推荐 │
└─────────────────────────────────────────────┘
1.3 实战配置
java
@Configuration
public class ChatMemoryConfig {
@Bean
public ChatMemory chatMemory() {
// 滑动窗口:保留最近20条
return MessageWindowChatMemory.builder()
.maxMessageCount(20)
.build();
}
@Bean
public ChatMemoryRepository chatMemoryRepository() {
// 生产环境换 JdbcChatMemoryRepository
return new InMemoryChatMemoryRepository();
}
}
使用示例:
java
@Service
public class CodeReviewService {
private final ChatMemory chatMemory;
private final ChatClient chatClient;
public String review(String conversationId, String code) {
// 追加用户消息
chatMemory.add(conversationId, List.of(
UserMessage.of("请审查:" + code)
));
// 获取历史(受滑动窗口控制)
List<ChatMessage> history = chatMemory.get(conversationId, 20);
// 调用 LLM
String response = chatClient.prompt()
.messages(history)
.options(ChatOptions.builder().model("qwen-plus").build())
.call()
.content();
// 追加 AI 响应
chatMemory.add(conversationId, List.of(
AssistantMessage.of(response)
));
return response;
}
}
1.4 生产环境注意
我的立场:不要在生产环境用InMemoryChatMemoryRepository,多实例部署时每个实例的记忆是隔离的,用户换实例就会丢上下文。
java
// 生产环境:JDBC 存储,支持多实例共享
@Bean
public ChatMemoryRepository chatMemoryRepository(DataSource dataSource) {
return new JdbcChatMemoryRepository(dataSource);
}
二、ReactAgent:基于 StateGraph 的 ReAct 循环实现
2.1 为什么选 ReactAgent?
Day4我调研了LangChain4j和Spring AI Alibaba两个方案,最终选ReactAgent,原因是它基于StateGraph实现,更容易嵌入现有架构。
表格
特性
LangChain4j
Spring AI Alibaba ReactAgent
架构风格
链式 (Chain)
图式 (StateGraph)
Spring集成
需要适配
原生支持
可嵌入性
独立运行
asNode() 可转为子图节点
2.2 核心架构:StateGraph 图状态机
ReactAgent将ReAct循环建模为状态图:
plaintext
┌──────────────────────────────────────────────┐
│ StateGraph │
│ ┌─────────────┐ ┌────────────────────┐ │
│ │AgentLlmNode │───▶│ AgentToolNode │ │
│ │ (推理节点) │ │ (工具执行) │ │
│ └─────────────┘ └────────────────────┘ │
│ │ │ │
│ └──────── Loop ────────┘ │
│ │
│ OverAllState: { messages: [...], ... } │
│ 终止条件: LLM响应不包含tool_call请求 │
└──────────────────────────────────────────────┘
2.3 实战代码
java
@Configuration
public class ReactAgentConfig {
@Bean
public ReactAgent codeReviewAgent(
ToolCallbackProvider toolCallbackProvider,
ChatMemory chatMemory) {
return ReactAgent.builder("代码审查Agent")
.model("qwen-plus")
.tools(toolCallbackProvider)
.chatMemory(chatMemory)
.stateful(true) // 状态保存
.build();
}
}
2.4 自定义工具节点
java
@Tool(name = "parseCode", description = "解析代码AST,提取函数和类结构")
public String parseCode(@ToolParam("待审查代码") String code) {
return parser.extractStructure(code);
}
@Tool(name = "checkSecurity", description = "安全漏洞扫描")
public String checkSecurity(@ToolParam("代码AST") String ast) {
return securityScanner.scan(ast);
}
@Tool(name = "generateReport", description = "生成审查报告")
public String generateReport(@ToolParam("审查结果") String result) {
return reportGenerator.format(result);
}
2.5 Hook 机制
Spring AI提供了四种HookPosition,我用AFTER_AGENT做了日志记录:
java
@Component
public class AgentLoggerHook implements ReactAgentHook {
@Override
public void afterAgent(AgentContext context, OverAllState state) {
log.info("Agent执行完成,消息数: {}", state.getMessages().size());
}
@Override
public HookPosition getPosition() {
return HookPosition.AFTER_AGENT;
}
}
2.6 终止条件
ReAct循环的退出逻辑:
java
while (true) {
Response llmResponse = callLlm(state);
if (!llmResponse.hasToolCalls()) {
// LLM认为任务完成,退出
return llmResponse;
}
// 执行工具,更新状态
state = executeTools(llmResponse.getToolCalls());
}
踩坑记录:Day4我遇到LLM死循环问题,根本原因是工具描述不够清晰。后来在@Tool的description里加上了明确的输入输出示例,问题解决。
三、ReflectiveReviewer:迭代式自我反思执行器
3.1 为什么需要自我反思?
代码审查质量不稳定——LLM有时漏检漏洞,有时格式混乱。我加了ReflectiveReviewer,让Agent主动审视输出,不达标就重来。
核心思想来自论文
Reflexion: Language Agents with Self-Reflection
。
3.2 迭代循环
plaintext
┌────────────────────────────────────────────┐
│ ReflectiveReviewer │
│ │
│ 审查1 → 反思1 → 改进 + 审查2 → ... │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ 终止条件 (3选1) │ │
│ │ 1. 质量达标 │ │
│ │ 2. 结果收敛 (Jaccard ≥ 0.85) │ │
│ │ 3. 达到最大迭代次数 (5轮) │ │
│ └──────────────────────────────────────┘ │
└────────────────────────────────────────────┘
3.3 核心实现
java
@Service
@Slf4j
public class ReflectiveReviewer {
private static final int MAX_ITERATIONS = 5;
private static final double CONVERGENCE_THRESHOLD = 0.85;
private final ReactAgent agent;
private final ReviewQualityChecker qualityChecker;
public ReviewResult iterativeReview(String code) {
String prompt = buildInitialPrompt(code);
ReviewResult bestResult = null;
List<String> previousIssues = new ArrayList<>();
for (int i = 0; i < MAX_ITERATIONS; i++) {
// 1. 调用Agent执行审查
String response = agent.execute(prompt);
ReviewResult current = parseResult(response);
// 2. 质量检查
if (qualityChecker.isAcceptable(current)) {
log.info("第 {} 轮审查通过", i + 1);
return current;
}
// 3. 收敛检查
if (bestResult != null) {
double similarity = calculateSimilarity(bestResult, current);
if (similarity >= CONVERGENCE_THRESHOLD) {
return bestResult;
}
}
// 4. 反思并生成改进prompt
prompt = buildReflectionPrompt(current, previousIssues);
bestResult = current;
previousIssues.addAll(extractIssues(current));
}
return bestResult;
}
// Jaccard相似度计算
private double calculateSimilarity(ReviewResult r1, ReviewResult r2) {
Set<String> issues1 = r1.getIssues().stream()
.map(i -> i.getTitle() + "|" + i.getDescription())
.collect(Collectors.toSet());
Set<String> issues2 = r2.getIssues().stream()
.map(i -> i.getTitle() + "|" + i.getDescription())
.collect(Collectors.toSet());
Set<String> intersection = new HashSet<>(issues1);
intersection.retainAll(issues2);
Set<String> union = new HashSet<>(issues1);
union.addAll(issues2);
return union.isEmpty() ? 1.0 : (double) intersection.size() / union.size();
}
private String buildReflectionPrompt(ReviewResult result, List<String> previousIssues) {
StringBuilder prompt = new StringBuilder();
prompt.append("请反思以下审查结果并改进:\n\n");
prompt.append("上次问题:\n");
result.getIssues().forEach(i ->
prompt.append("- ").append(i.getTitle()).append(": ")
.append(i.getDescription()).append("\n"));
if (!previousIssues.isEmpty()) {
prompt.append("\n历史遗漏:\n");
previousIssues.forEach(i -> prompt.append("- ").append(i).append("\n"));
}
prompt.append("\n请重新审查,确保识别所有问题、提供可操作建议、评估质量分数");
return prompt.toString();
}
}
3.4 三个终止条件的权衡
表格
条件
优点
缺点
质量达标
精确控制输出
需要精心设计规则,可能被"聪明"的LLM绕过
结果收敛
自动化判断
相似度高不等于质量高
最大迭代5轮
兜底保护
经验值,需根据实际调整
3.5 改进空间
回顾这段代码,我发现三个可以优化的地方:
1. Jaccard应替换为语义相似度
java
// 当前:基于文本的Jaccard
// 更好的方案:Embedding计算语义相似度
@Autowired
private EmbeddingModel embeddingModel;
private double semanticSimilarity(ReviewResult r1, ReviewResult r2) {
String text1 = serializeIssues(r1.getIssues());
Embedding emb1 = embeddingModel.embed(text1);
Embedding emb2 = embeddingModel.embed(serializeIssues(r2.getIssues()));
return cosineSimilarity(emb1, emb2);
}
2. 反思Prompt应动态填充缺失维度
当前是固定模板,更智能的做法是根据检查结果动态生成针对性反思指令:
java
private String buildSmartReflectionPrompt(ReviewResult result, Set<String> missingDimensions) {
StringBuilder prompt = new StringBuilder("请重新审查,注意以下改进点:\n");
if (missingDimensions.contains("issues")) {
prompt.append("- 上次遗漏了部分问题,请更仔细检查\n");
}
if (missingDimensions.contains("score")) {
prompt.append("- 请重新评估代码质量分数\n");
}
// ...
return prompt.toString();
}
3. previousIssues应融入Agent Prompt
我发现previousIssues提取了但没传给Agent。应该作为System Prompt的一部分:
java
String systemPrompt = """
你是一个严谨的代码审查助手。
请注意历史审查中曾被遗漏的问题类型,避免重蹈覆辙:
%s
""".formatted(String.join("\n", previousIssues));
agent.setSystemPrompt(systemPrompt);
四、架构串联:三个组件如何协作
plaintext
┌─────────────────────────────────────────────────────┐
│ code-review-agent │
│ │
│ ┌───────────────────────────────────────────────┐ │
│ │ ReflectiveReviewer │ │
│ │ (迭代控制器: 最多5轮审查+反思) │ │
│ └───────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────┐ │
│ │ ReactAgent │ │
│ │ AgentLlmNode ──▶ AgentToolNode │ │
│ │ (推理) parseCode/checkSecurity │ │
│ └───────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────┐ │
│ │ ChatMemory │ │
│ │ MessageWindowChatMemory (20条) │ │
│ │ └─ ChatMemoryRepository (JDBC生产) │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
数据流向:
- 用户请求 →
ReflectiveReviewer开始迭代 - 每轮调用
ReactAgent→AgentLlmNode推理 AgentToolNode执行工具(代码解析、安全扫描、报告生成)- 工具结果通过
OverAllState回传给推理节点 ChatMemory维护对话上下文- 迭代结束 → 输出最终审查报告
五、实际效果展示
结语
回顾这5天的演进,我最大的感受是:Spring AI生态在快速成熟,但文档和最佳实践仍然稀缺。很多设计理念(比如Hook机制、asNode嵌入)需要读源码才能理解。