LangChain4j 记忆与 Agent:从 ChatMemory 到自主 Agent
导读:大模型本身是无状态的,每次调用都是独立的请求-响应。要让 AI 具备"记住上文"的能力,就需要记忆管理;要让 AI 具备"自主决策"的能力,就需要 Agent 执行循环。本文从 LangChain4j 的 ChatMemory 出发,依次讲解内存记忆、持久化记忆与多用户会话隔离,再深入到 ReAct 执行循环、多步推理 Agent,最后梳理生产上线前的必备检查清单。读完本文,你将具备用 LangChain4j 构建一套完整 AI Agent 系统的能力。
一、ChatMemory:为什么 AI 会"失忆"?
1.1 问题场景
用户问:"上一条消息里提到的 JDK 21 虚拟线程,具体特性是什么?"没有记忆的 AI 会一脸茫然地回答:"我没提过。"这在实际项目中非常常见——用户投诉 AI 太笨,很多时候就是因为没有做记忆管理。
1.2 核心原理
大模型每次调用都是无状态的,它不知道上次说了什么。要实现多轮对话,本质就是把历史对话一起发给模型。ChatMemory 的作用就是自动管理这些历史消息列表,包括:
- 自动追加:每轮对话的用户消息和 AI 回复自动加入历史
- 自动截断:超过窗口大小时自动丢弃最老的消息
- 自动保护:System Message 始终保留,不会被截断挤掉
1.3 两种窗口策略
LangChain4j 提供了两种开箱即用的记忆策略:
MessageWindowChatMemory:基于消息条数
ChatMemory memory = MessageWindowChatMemory.withMaxMessages(20);
保留最近 N 条消息,超过就删除最老的一条。适用于消息长度比较均匀的场景,开发快速原型时优先选择。
TokenWindowChatMemory:基于 Token 数量
ChatMemory memory = TokenWindowChatMemory.builder()
.maxTokens(4096)
.build();
保留最近不超过指定 Token 数的消息。适用于消息长度差异大的场景,需要精确控制上下文窗口时使用。
选型建议:
| 场景 | 推荐策略 |
|---|---|
| 消息长度均匀 | MessageWindow |
| 消息长度差异大 | TokenWindow |
| 需要严格控制上下文 | TokenWindow |
| 快速原型开发 | MessageWindow |
1.4 会话隔离:@MemoryId
如果所有用户共用一个 ChatMemory,你刚问完的问题,别人再问时就会"串台"——这显然不行。LangChain4j 通过 @MemoryId 注解实现会话隔离。项目中的 ChatAssistant 接口(com.jichi.langchain4j.service.chatMemory.ChatAssistant)就是这样定义的:
public interface ChatAssistant {
@SystemMessage("你是一个 Java 技术助手,记住用户在对话中提到的技术栈和问题背景")
String chat(@MemoryId String sessionId, @UserMessage String message);
}
只要 sessionId 不同,对话历史就是完全隔离的。配合 ChatMemoryProvider 使用,项目中的 ChatMemoryConfig(com.jichi.langchain4j.config.ChatMemoryConfig)展示了最简配置:
@Configuration
public class ChatMemoryConfig {
// 提供全局 ChatMemoryProvider,所有 @AiService 使用 @MemoryId 时都会用到
@Bean
public ChatMemoryProvider chatMemoryProvider() {
return memoryId -> MessageWindowChatMemory.withMaxMessages(10);
}
}
它会根据不同的 memoryId 自动创建独立的会话,维护各自的历史消息。
1.5 System Message 的安全性
有人担心 System Message 会不会随着对话越来越多被挤掉。放心,LangChain4j 做了特殊处理:
- System Message 始终只保留一条,不会累积
- 窗口满了时只会丢弃 User 或 AI 的消息,System Message 永远不丢
二、持久化记忆:对接数据库与多用户隔离
2.1 为什么需要持久化?
上面的 ChatMemory 默认存在内存里。服务重启、宕机后,所有对话记录全部丢失。要在生产环境中使用,必须把记忆持久化到数据库。
2.2 实现 ChatMemoryStore 接口
LangChain4j 默认使用 InMemoryChatMemoryStore,我们只需实现 ChatMemoryStore 接口即可切换到数据库存储:
public interface ChatMemoryStore {
List<ChatMessage> getMessages(Object memoryId);
void updateMessages(Object memoryId, List<ChatMessage> messages);
void deleteMessages(Object memoryId);
}
三个方法职责清晰:获取消息、更新消息、删除消息。
2.3 JPA 实体与表设计
项目中使用 ChatMessageEntity(com.jichi.langchain4j.model.ChatMessageEntity)作为 JPA 实体,对应 chat_message 表:
@Entity
@Table(name = "chat_message")
@Data
@NoArgsConstructor
public class ChatMessageEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String sessionId;
@Column(nullable = false)
private String role; // SYSTEM / USER / AI / TOOL
@Column(columnDefinition = "TEXT", nullable = false)
private String content;
private String toolName; // 工具调用消息时的工具名
@CreationTimestamp
private LocalDateTime createdAt;
}
核心字段:sessionId 用于会话隔离,role 标识消息来源(SYSTEM、USER、AI、TOOL),content 存储消息内容,toolName 记录工具调用信息。
2.4 Repository 层
ChatMessageRepository(com.jichi.langchain4j.repository.ChatMessageRepository)定义按 sessionId 查询、删除、以及按前缀查询的方法:
@Repository
public interface ChatMessageRepository extends JpaRepository<ChatMessageEntity, Long> {
List<ChatMessageEntity> findBySessionIdOrderByCreatedAtAsc(String sessionId);
void deleteBySessionId(String sessionId);
// 查询以指定前缀开头的所有不重复 sessionId(用于按用户查历史会话)
@Query("SELECT DISTINCT e.sessionId FROM ChatMessageEntity e WHERE e.sessionId LIKE :prefix%")
List<String> findDistinctSessionIdsByPrefix(@Param("prefix") String prefix);
}
2.5 JPA 实现 ChatMemoryStore
项目中的 JpaChatMemoryStore(com.jichi.langchain4j.memory.JpaChatMemoryStore)是持久化的核心实现:
@Component
public class JpaChatMemoryStore implements ChatMemoryStore {
private final ChatMessageRepository repository;
public JpaChatMemoryStore(ChatMessageRepository repository) {
this.repository = repository;
}
@Override
public List<ChatMessage> getMessages(Object memoryId) {
return repository.findBySessionIdOrderByCreatedAtAsc(memoryId.toString())
.stream()
.map(this::toMessage)
.filter(Objects::nonNull)
.toList();
}
@Override
@Transactional
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
// 全量替换:先删后插
// 生产环境可以优化为增量更新(只追加新消息),减少写放大
repository.deleteBySessionId(memoryId.toString());
List<ChatMessageEntity> entities = messages.stream()
.map(msg -> toEntity(memoryId.toString(), msg))
.toList();
repository.saveAll(entities);
}
@Override
@Transactional
public void deleteMessages(Object memoryId) {
repository.deleteBySessionId(memoryId.toString());
}
private ChatMessageEntity toEntity(String sessionId, ChatMessage message) {
ChatMessageEntity entity = new ChatMessageEntity();
entity.setSessionId(sessionId);
if (message instanceof SystemMessage m) {
entity.setRole("SYSTEM");
entity.setContent(m.text());
} else if (message instanceof UserMessage m) {
entity.setRole("USER");
entity.setContent(m.singleText());
} else if (message instanceof AiMessage m) {
entity.setRole("AI");
entity.setContent(m.text() != null ? m.text() : "");
} else if (message instanceof ToolExecutionResultMessage m) {
entity.setRole("TOOL");
entity.setContent(m.text());
entity.setToolName(m.toolName());
}
return entity;
}
private ChatMessage toMessage(ChatMessageEntity entity) {
return switch (entity.getRole()) {
case "SYSTEM" -> new SystemMessage(entity.getContent());
case "USER" -> new UserMessage(entity.getContent());
case "AI" -> new AiMessage(entity.getContent());
case "TOOL" -> new ToolExecutionResultMessage(
entity.getToolName(), entity.getToolName(), entity.getContent());
default -> null;
};
}
}
这里有几个关键设计:toEntity 方法处理了 LangChain4j 的四种消息类型(SystemMessage、UserMessage、AiMessage、ToolExecutionResultMessage),toMessage 方法使用 Java 17 的 switch 表达式做反向转换。updateMessages 采用"先删后插"的全量替换策略,生产环境可优化为增量更新。
2.6 绑定到 ChatMemoryProvider
PersistentMemoryConfig(com.jichi.langchain4j.config.PersistentMemoryConfig)展示了如何把 JPA Store 绑定到 AI Service:
@Configuration
public class PersistentMemoryConfig {
@Bean
public PersistentChatAssistant chatAssistant(ChatModel model, JpaChatMemoryStore memoryStore) {
return AiServices.builder(PersistentChatAssistant.class)
.chatModel(model)
.chatMemoryProvider(memoryId ->
MessageWindowChatMemory.builder()
.id(memoryId)
.maxMessages(20)
.chatMemoryStore(memoryStore) // 关键:绑定持久化 Store
.build())
.build();
}
}
只需在 chatMemoryStore() 处绑定自定义的 JpaChatMemoryStore,所有聊天记录就会自动持久化到数据库。即使服务重启,只要 sessionId 保持一致,AI 就能"记住"之前的对话。
2.7 多用户会话管理
生产环境中需要管理多个用户的多个会话。项目中的 SessionManagementService(com.jichi.langchain4j.service.chatMemory.SessionManagementService)给出了完整的实现:
@Service
public class SessionManagementService {
private final ChatMessageRepository messageRepository;
private final JpaChatMemoryStore memoryStore;
public SessionManagementService(ChatMessageRepository messageRepository,
JpaChatMemoryStore memoryStore) {
this.messageRepository = messageRepository;
this.memoryStore = memoryStore;
}
/**
* 查询用户的所有历史会话(用第一条 USER 消息作为摘要)
*/
public List<SessionSummary> getUserSessions(String userId) {
String prefix = userId + "_";
return messageRepository.findDistinctSessionIdsByPrefix(prefix)
.stream()
.map(sessionId -> {
List<ChatMessageEntity> messages =
messageRepository.findBySessionIdOrderByCreatedAtAsc(sessionId);
String summary = messages.stream()
.filter(m -> "USER".equals(m.getRole()))
.findFirst()
.map(m -> m.getContent().substring(0, Math.min(50, m.getContent().length())))
.orElse("新对话");
LocalDateTime lastActive = messages.isEmpty()
? LocalDateTime.now()
: messages.get(messages.size() - 1).getCreatedAt();
return new SessionSummary(sessionId, summary, lastActive);
})
.toList();
}
/**
* 删除会话(校验归属权,防止越权删除)
*/
public void deleteSession(String sessionId, String userId) {
if (!sessionId.startsWith(userId + "_")) {
throw new SecurityException("无权删除此会话");
}
memoryStore.deleteMessages(sessionId);
}
/**
* 生成新会话 ID
*/
public String newSession(String userId) {
return userId + "_" + System.currentTimeMillis();
}
public record SessionSummary(String sessionId, String summary, LocalDateTime lastActive) {
}
}
核心设计思路:用 userId + "_" + 时间戳 拼接 sessionId。这样做有几个好处:
- 天然支持按用户前缀查询:通过
findDistinctSessionIdsByPrefix用LIKE 'userId_%'查到该用户所有会话 - 查询用户历史会话列表:取出所有 sessionId,用第一条 USER 消息作为摘要展示
- 删除会话时校验归属:检查 sessionId 是否以当前
userId + "_"开头,防止越权操作
三、Agent 执行循环:ReAct 模式
3.1 普通 AI 调用 vs Agent
- 普通 AI 调用:请求 -> 模型 -> 响应,一问一答就结束了
- Agent:模型可以自主决定是否调用工具,工具结果再反馈给模型,模型再决定下一步。这个循环持续进行,直到任务完成
这就是 ReAct(Reasoning + Acting)模式的核心思想:
用户提问 -> 模型推理(Thought) -> 决定调用工具(Action) -> 工具执行返回结果(Observation)
-> 模型再推理 -> 再调用工具 -> ... -> 给出最终答案
3.2 LangChain4j 中的 Agent 执行流程
- 用户消息进入,构建 Message 列表
- 所有 Tool 定义转化为模型可理解的格式,一并发送给模型
- 模型返回结果:
- 如果直接回答(无工具调用),流程结束
- 如果需要调用工具,进入循环 - 执行工具调用,获取结果
- 将工具结果作为新的 Message 加入列表,再次调用模型
- 重复 3-5,直到模型给出最终答案,或达到最大循环次数
3.3 构建一个完整的 Agent
先准备工具类。项目中在 com.jichi.langchain4j.tools.agent 包下定义了三个独立的工具类:
天气工具 CityWeatherTools:
@Component
public class CityWeatherTools {
@Tool("查询城市实时天气,返回天气状况和温度")
public String getWeather(@P("城市名称") String city) {
return city + ":晴天,18°C,风力2级";
}
}
搜索工具 WebSearchTools:
@Component
public class WebSearchTools {
@Tool("搜索互联网获取信息,适用于查询实时新闻、百科知识等")
public String search(@P("搜索关键词") String query) {
return "搜索结果:关于 " + query + " 的相关信息:这是一段模拟的搜索结果内容。";
}
}
计算器工具 ArithmeticMathTools:
@Component
public class ArithmeticMathTools {
@Tool("执行简单的数学计算,仅支持加减乘除")
public double calculate(
@P("第一个数字") double a,
@P("运算符:+、-、*、/") String op,
@P("第二个数字") double b) {
return switch (op) {
case "+" -> a + b;
case "-" -> a - b;
case "*" -> a * b;
case "/" -> b != 0 ? a / b : Double.NaN;
default -> throw new IllegalArgumentException("不支持的运算符:" + op);
};
}
}
然后定义 Agent 接口 SmartAgent(com.jichi.langchain4j.service.agent.SmartAgent),通过 System Prompt 告知模型拥有哪些能力:
public interface SmartAgent {
@SystemMessage("""
你是一个智能助手,拥有以下工具:
- 查天气:可以获取任何城市的实时天气
- 搜索:可以搜索互联网获取信息
- 计算:可以执行加减乘除运算
根据用户的问题,决定是否需要使用工具。
需要多个信息时,可以多次调用工具。
综合所有工具的返回结果,给出准确的最终答案。
""")
String chat(@MemoryId String sessionId, @UserMessage String message);
}
最后通过 SmartAgentConfig(com.jichi.langchain4j.config.SmartAgentConfig)组装:
@Configuration
public class SmartAgentConfig {
@Bean
public SmartAgent smartAgent(
ChatModel chatModel,
CityWeatherTools weatherTools,
WebSearchTools searchTools,
ArithmeticMathTools mathTools) {
return AiServices.builder(SmartAgent.class)
.chatModel(chatModel)
// unwrap 剥掉 Spring AOP 的 CGLIB 代理,让 LangChain4j 能扫到 @Tool
.tools(unwrap(weatherTools), unwrap(searchTools), unwrap(mathTools))
.chatMemoryProvider(memoryId ->
MessageWindowChatMemory.withMaxMessages(20))
.build();
}
private Object unwrap(Object bean) {
if (AopUtils.isAopProxy(bean) && bean instanceof Advised advised) {
try {
return advised.getTargetSource().getTarget();
} catch (Exception e) {
throw new RuntimeException("无法解包 AOP 代理:" + bean.getClass(), e);
}
}
return bean;
}
}
注意这里的 unwrap 方法——这是一个容易踩的坑:Spring AOP 会给 @Component 加 CGLIB 代理,而 LangChain4j 通过反射扫描 @Tool 注解时拿不到原始类,必须手动剥掉代理层。
当用户同时问"北京天气怎么样"和"168 乘以 3 等于多少"时,Agent 会依次调用天气工具和计算器工具,然后综合给出结果。
3.4 监听 Agent 执行过程
项目中提供了两种监听方式。一是 ChatModelLoggingConfig(com.jichi.langchain4j.listener.ChatModelLoggingConfig),监控模型的请求和响应:
@Configuration
public class ChatModelLoggingConfig {
@Bean
public ChatModelListener loggingListener() {
return new ChatModelListener() {
@Override
public void onRequest(ChatModelRequestContext ctx) {
System.out.println("\n===== 【发送给模型】 =====");
ctx.chatRequest().messages().forEach(m ->
System.out.println("[" + m.type() + "] " + m));
if (ctx.chatRequest().toolSpecifications() != null) {
System.out.println("可用工具:" +
ctx.chatRequest().toolSpecifications().stream()
.map(t -> t.name())
.toList());
}
}
@Override
public void onResponse(ChatModelResponseContext ctx) {
System.out.println("\n===== 【模型返回】 =====");
var response = ctx.chatResponse().aiMessage();
if (response.text() != null) {
System.out.println("回答:" + response.text());
}
if (response.hasToolExecutionRequests()) {
response.toolExecutionRequests().forEach(t ->
System.out.println("调用工具:" + t.name() + ",参数:" + t.arguments()));
}
}
@Override
public void onError(ChatModelErrorContext ctx) {
System.out.println("===== 【模型报错】 =====");
ctx.error().printStackTrace();
}
};
}
}
二是 ToolTraceLogger(com.jichi.langchain4j.listener.ToolTraceLogger),专门追踪每次工具调用的详情:
@Component
@Slf4j
public class ToolTraceLogger implements Consumer<ToolExecution> {
@Override
public void accept(ToolExecution execution) {
System.out.println(String.format(
">>> [ToolTraceLogger] 工具名:%s,参数:%s,结果:%s",
execution.request().name(),
execution.request().arguments(),
execution.result()));
log.info("[工具追踪] 工具名:{},参数:{},结果:{}",
execution.request().name(),
execution.request().arguments(),
execution.result());
}
}
通过这两层日志,可以清晰看到:模型收到了哪些工具定义、用户问了什么、模型决定调用哪个工具、工具的入参和返回值、最终答案是什么。这对调试 Agent 行为非常有帮助。
四、自主 Agent:多步推理与工具链自动组合
4.1 简单任务 vs 复杂任务
- 简单任务:查天气,调一个工具就搞定
- 复杂任务:分析苹果公司最新财报,结合当前股价给出投资建议。需要拆解为多个子任务——搜索最新财报、查询当前股价、查询近期走势、综合分析给建议——并且这些子任务之间存在依赖关系
Agent 需要自主决定调用顺序,逐步收集信息,最后综合判断。这是 LangChain4j 与普通 ChatGPT 调用的最大分水岭。
4.2 关键一:精心设计 System Prompt
对于复杂 Agent,System Prompt 不能只写"你是一个专业的 XX 助手",需要明确告诉模型如何思考。项目中的 AnalysisAgent(com.jichi.langchain4j.service.ownerAgent.AnalysisAgent)展示了一个优秀的 System Prompt 设计:
public interface AnalysisAgent {
@SystemMessage("""
你是一个专业的财务分析助手,能够进行多步骤的深度分析。
工作方式:
1. 分析用户问题,识别需要哪些信息
2. 按逻辑顺序调用工具收集数据(先收集基础数据,再做依赖前者的分析)
3. 每次工具调用后,评估信息是否充足
4. 信息充足后,综合所有数据给出完整分析
重要原则:
- 不要基于假设数据,只使用工具返回的真实数据
- 如果某个工具调用失败,说明该数据暂时不可用,基于现有数据做部分分析
- 最终答案要有数据支撑,不要说"大约"、"可能"这类模糊表述
""")
String analyze(@MemoryId String sessionId, @UserMessage String question);
}
4.3 关键二:工具粒度的黄金法则
工具设计中有一个常见的两难问题:
- 粒度太粗:一个工具什么都能干,模型容易失控
- 粒度太细:工具太多,模型不知道选哪个
黄金法则是每个工具只做一件清晰的事。项目中的 FinanceDataTools(com.jichi.langchain4j.tools.ownerAgent.FinanceDataTools)就遵循了这一原则:
@Component
public class FinanceDataTools {
@Tool("获取指定股票的实时价格")
public String getStockPrice(@P("股票代码,如 AAPL、600036") String symbol) {
// 模拟数据,真实项目对接行情 API
return symbol + " 当前价格:168.42 USD,涨跌:+1.23%";
}
@Tool("获取指定股票最近 N 天的价格走势,返回收盘价列表")
public String getStockHistory(
@P("股票代码") String symbol,
@P("查询天数,1-90 之间") int days) {
return symbol + " 近 " + days + " 天收盘价:[162.5, 164.3, 165.1, 166.8, 168.4](模拟数据)";
}
@Tool("获取指定公司的核心财务指标:市盈率、营收、净利润")
public String getFinancials(@P("公司名称或股票代码") String company) {
return company + " 财务指标:市盈率 28.5,TTM 营收 3830 亿 USD,净利润 970 亿 USD";
}
@Tool("搜索关于指定主题的最新新闻摘要")
public String searchNews(@P("搜索主题,建议带公司名或股票代码") String topic) {
return topic + " 近期新闻:(1) 新品发布会获市场正面反应;(2) AI 布局持续加码;(3) 分析师维持买入评级(均为模拟数据)";
}
}
四个工具各司其职:查实时价格、查历史走势、查财务指标、搜新闻。@Tool 的描述要精确——模型就是靠这段文字来决定该调哪个工具的。
组装时通过 AnalysisAgentConfig(com.jichi.langchain4j.config.AnalysisAgentConfig)将工具绑定到 Agent:
@Configuration
public class AnalysisAgentConfig {
@Bean
public AnalysisAgent analysisAgent(ChatModel chatModel, FinanceDataTools financeTools) {
return AiServices.builder(AnalysisAgent.class)
.chatModel(chatModel)
.tools(unwrap(financeTools))
// 多步推理上下文长,多留一些消息窗口
.chatMemoryProvider(memoryId ->
MessageWindowChatMemory.withMaxMessages(30))
.build();
}
// ... unwrap 方法同上
}
注意这里 maxMessages 设为 30——多步推理场景下每次工具调用都会产生 ToolExecutionRequest 和 ToolExecutionResult 两条消息,窗口设太小会导致前几步的工具结果被丢弃,Agent 后续步骤"失忆"。
4.4 异步提交 + 轮询:优化响应体验
复杂 Agent 执行多步推理时耗时较长,让用户傻等不是好体验。项目中的 AsyncAnalysisService(com.jichi.langchain4j.service.ownerAgent.AsyncAnalysisService)采用了异步提交 + 轮询的方式:
@Service
public class AsyncAnalysisService {
private final AnalysisAgent agent;
private final Map<String, TaskStatus> taskMap = new ConcurrentHashMap<>();
public AsyncAnalysisService(AnalysisAgent agent) {
this.agent = agent;
}
public String submitTask(String question) {
String taskId = UUID.randomUUID().toString();
taskMap.put(taskId, new TaskStatus("RUNNING", null, null));
CompletableFuture.supplyAsync(() -> agent.analyze(taskId, question))
.whenComplete((result, ex) -> {
if (ex != null) {
taskMap.put(taskId, new TaskStatus("FAILED", null, ex.getMessage()));
} else {
taskMap.put(taskId, new TaskStatus("DONE", result, null));
}
});
return taskId;
}
public TaskStatus getStatus(String taskId) {
return taskMap.getOrDefault(taskId, new TaskStatus("NOT_FOUND", null, null));
}
public record TaskStatus(String status, String result, String error) {
}
}
Controller 层(AsyncAnalysisController)提供两个端点:
@RestController
@RequestMapping("/analysis/async")
public class AsyncAnalysisController {
private final AsyncAnalysisService asyncService;
public AsyncAnalysisController(AsyncAnalysisService asyncService) {
this.asyncService = asyncService;
}
@PostMapping("/submit")
public Map<String, String> submit(@RequestBody Map<String, String> req) {
String taskId = asyncService.submitTask(req.get("question"));
return Map.of("taskId", taskId, "message", "任务已提交,请用 taskId 轮询状态");
}
@GetMapping("/status/{taskId}")
public AsyncAnalysisService.TaskStatus getStatus(@PathVariable String taskId) {
return asyncService.getStatus(taskId);
}
}
提交任务立即返回 taskId,前端用 taskId 轮询 /status 端点直到状态变为 DONE 或 FAILED。
4.5 调试复杂 Agent 的技巧
- 零件隔离测试:先单独测试每个工具是否正常工作,再组装成 Agent
- 关注返回格式:90% 的多步推理问题出在工具返回格式上,模型看不懂就会跑偏
- 记忆窗口要够大:
maxMessages设太小会导致前几步的工具结果被丢弃,Agent 后续步骤"失忆"。建议多步推理场景下设为 30 条以上(如AnalysisAgentConfig中所示) - 用 Listener 追踪:通过
ChatModelLoggingConfig和ToolTraceLogger记录每一步的输入输出,快速定位问题
五、生产注意事项:上线前的六道防线
把 Agent 应用推到生产环境,需要系统性地考虑以下六个维度。
5.1 安全:API Key 管理
API Key 不要写在代码里,不要写在代码里,不要写在代码里。
也不要明文提交到 Git 仓库。正确做法:
- 使用环境变量注入
- 使用公司的密钥管理服务(如 Vault)
- 本地开发可以用
.env文件(加入.gitignore)
5.2 限流:防止账单爆炸
不做限流,被恶意攻击时 API 账单会直接炸掉。推荐双重限流策略:
| 层级 | 策略 | 示例 |
|---|---|---|
| 用户级 | 分钟级限流 | 每分钟 5 次 |
| 全局级 | 秒级限流 | 每秒 10 次 |
可以使用 Guava 的 RateLimiter 实现,基本覆盖大部分场景。
5.3 降级与容灾:模型挂了怎么办
- 备用模型切换:主模型不可用时自动切换到备用模型
- 重试机制:对临时性错误进行有限次数的重试
- 优雅降级:极端情况下返回预设的兜底回答,而不是直接报错
5.4 成本控制:Token 就是钱
每次调用的 Token 消耗 = 历史消息 + 用户消息 + RAG 检索文档片段 + System Prompt。控制成本的方法:
- System Prompt 精炼:它每次都在,废话越多成本越高
- 记忆窗口别太大:按需设置,不要无脑设 100 条
- RAG 检索控制:
topK设 3-5 条就够,不要贪多 - 模型分级使用:简单任务用便宜的小模型,复杂任务才用旗舰模型
- Token 监控告警:设置日/月消耗阈值,超过即告警
5.5 超时配置
Agent 执行多步推理时耗时较长,需要给足思考时间。建议先统计 Agent 的平均响应时间,再设置合理的超时阈值(通常 30-60 秒),避免过早中断正常的多步推理。
5.6 并发安全
LangChain4j 的 AI Service 代理本身是线程安全的,但工具类需要特别注意。一个常见的反例:
// 错误示范:共享状态
@Component
public class SearchTool {
private String lastQuery; // 共享变量,多线程会互相覆盖!
@Tool("搜索")
public String search(String query) {
this.lastQuery = query; // 危险!
return doSearch(query);
}
}
正确做法:工具类保持无状态,所有数据都用局部变量或线程安全的容器。
5.7 生产上线检查清单
| 检查项 | 要求 |
|---|---|
| API Key | 环境变量或密钥管理服务,不在代码中明文出现 |
| 限流 | 用户级 + 全局级双重限流 |
| 降级 | 备用模型切换方案就绪 |
| 成本 | Token 监控告警已配置 |
| 超时 | 合理的超时配置 |
| 并发 | 工具类无状态,无共享可变变量 |
| 版本 | LangChain4j 版本锁定(迭代快,避免升级破坏) |
5.8 版本管理建议
LangChain4j 迭代速度很快,API 变动频繁。务必在 pom.xml 或 build.gradle 中锁定版本号,不要使用 LATEST 或范围版本,避免自动升级导致构建失败。
六、总结
本文从 LangChain4j 的记忆管理讲到自主 Agent,再到生产上线的注意事项,完整覆盖了构建一个可靠 AI Agent 系统的全链路:
- ChatMemory:通过 MessageWindow 或 TokenWindow 策略管理对话历史,用
@MemoryId实现会话隔离,System Message 始终受保护 - 持久化记忆:通过
JpaChatMemoryStore实现ChatMemoryStore接口对接 MySQL,用userId + 时间戳的 sessionId 方案配合SessionManagementService实现多用户会话管理 - ReAct 执行循环:Agent 的核心——推理、行动、观察的循环,让 AI 从"一问一答"进化为"自主决策"
- 多步推理 Agent:精心设计 System Prompt(如
AnalysisAgent)和工具粒度(如FinanceDataTools),配合AsyncAnalysisService异步轮询优化用户体验 - 生产六道防线:安全、限流、降级、成本、超时、并发,缺一不可
简单场景用 Spring AI,复杂场景上 LangChain4j——两套方案在手,足以应对绝大多数 AI 应用开发需求。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)