从0搭建你的第一个 AI Agent:LangChain4j 架构总览+最小可运行示例
引言
"我们想做一个 AI 客服"——产品同学在需求会上说。"简单,接个 GPT API 不就行了?"——这是 90% 团队的第一反应。然后做完发现:用户说"帮我查一下订单 SO20250912001 的物流",模型回了一句"作为 AI 助手,我无法查询您的订单"。
问题出在哪?接了个聊天 API,不是做了个 Agent。聊天 API 只能基于训练知识"说话",不能"做事"——查不了数据库、调不了接口、记不住用户身份。Agent 的本质区别不是"更聪明的聊天",而是"LLM + 工具 + 记忆 + 规划"四件套的组合体:LLM 是大脑,工具是手脚,记忆是上下文,规划是行动方案。
这篇文章是 AI Agent 系列的开篇,用 LangChain4j 拆解 Agent 的四层架构,最后给一个能跑的最小 Agent——它能在"我帮不了你"时主动调工具查数据库,在多轮对话中记住你是谁。全程纯 Java,不写一行 Python。
一、Agent 的本质:不是聊天,是行动
1.1 聊天 vs Agent 的区别
用户:帮我查一下订单 SO20250912001 的物流状态
聊天 API(gpt-4o generate()):
→ "作为 AI 助手,我无法访问您的订单系统..."
→ 模型只能"说话",不能"做事"
Agent:
→ LLM 理解意图:"用户要查物流" → 选择工具 getLogisticsStatus
→ 框架执行 getLogisticsStatus("SO20250912001") → 查数据库 → 物流信息
→ LLM 拿到结果 → 组织回答 → "您的订单已发货,预计明天送达"
→ 模型不仅"说话",还能"做事"
Agent 的本质:LLM 不直接回答用户,而是先"决定要不要调工具"→ 框架执行工具 → 把结果喂回 LLM → LLM 基于结果生成最终回答。这个"思考→行动→观察→回答"的循环就是 Agent 的主循环(ReAct 模式)。
1.2 四层架构
┌──────────────────────────────────────────────┐
│ 编排层(Agent 主循环) │
│ 思考 → 选工具 → 执行 → 观察 → 再思考 → 回答 │
├──────────────────────────────────────────────┤
│ 记忆层 工具层 │
│ 短期:对话历史 Function Calling │
│ 长期:用户画像/偏好 查数据库/调API/发邮件 │
├──────────────────────────────────────────────┤
│ 模型层(LLM) │
│ GPT-4o / DeepSeek / Qwen ← 大脑 │
└──────────────────────────────────────────────┘
| 层 | 职责 | LangChain4j 对应 |
|---|---|---|
| 模型层 | 理解意图、选择工具、生成回答 | ChatLanguageModel |
| 工具层 | 执行实际动作(查 DB、调 API) | @Tool 注解 + Function Calling |
| 记忆层 | 保持对话上下文和长期信息 | ChatMemory / 持久化存储 |
| 编排层 | 协调模型-工具-记忆的循环 | AiServices 自动代理 |
二、模型层:LLM 是大脑
2.1 Agent 为什么要用 LLM
传统规则引擎也能"查物流"——if (意图==查物流) { 调物流接口 }。但规则引擎的问题是:用户说"我的包裹到哪了"、"帮我看看那个单子的情况"、"上周买的东西到了没"——三种说法,同一个意图,规则引擎要给每种说法写一条规则。LLM 的价值是理解自然语言的多样性,映射到有限的工具集合。
2.2 模型选型
| 维度 | 推荐 | 理由 |
|---|---|---|
| Function Calling 支持 | gpt-4o / deepseek-chat / qwen-plus | 不是所有模型都支持 Tool Calling,选型第一关 |
| 推理能力 | gpt-4o > deepseek > qwen | Agent 需要多步推理,能力差的模型会选错工具 |
| 延迟 | deepseek-chat 首 token ~1.5s | 流式场景体感关键 |
| 成本 | deepseek ¥1/M token vs gpt-4o $2.5/M | Agent 每轮调多次模型,成本差 10 倍 |
选型建议:开发期用 DeepSeek(便宜+Function Calling 完整),生产核心场景用 gpt-4o(推理最强),简单路由用 qwen-plus。
ChatLanguageModel model = OpenAiChatModel.builder()
.apiKey(System.getenv("DEEPSEEK_API_KEY"))
.baseUrl("https://api.deepseek.com/v1")
.modelName("deepseek-chat")
.temperature(0.2) // Agent 场景 Temperature 要低(选工具要确定性)
.timeout(Duration.ofSeconds(60))
.build();
Temperature 为什么设 0.2:Agent 的核心是"选对工具"——这是个决策任务不是创意任务,确定性 > 多样性。Temperature=0.7 时模型可能"创意地"选了不该选的工具。
三、工具层:Function Calling 是手脚
3.1 工具的本质
用户:"帮我查订单 SO20250912001"
↓
LLM 看到可用工具列表 → 选择 getOrderByOrderNo
↓ 输出:{"name": "getOrderByOrderNo", "arguments": {"orderNo": "SO20250912001"}}
框架执行 → 调 Java 方法 → 查 DB → 返回结果
↓ 结果:{"status": "已发货", "logistics": "顺丰 SF1234567", "eta": "明天下午"}
LLM 拿到结果 → 组织回答:"您的订单已发货,顺丰单号 SF1234567,预计明天下午送达。"
LLM 不直接调方法——它"说出"要调哪个方法+参数,框架执行后把结果喂回去。这是 Function Calling 的核心机制(上一篇 LangChain4j 生态文已详细讲过,这里聚焦 Agent 视角)。
3.2 @Tool 注解定义工具
@Component
public class OrderTools {
private final OrderService orderService;
private final LogisticsService logisticsService;
/** 工具1:查订单 */
@Tool("根据订单号查询订单状态和物流信息")
public OrderInfo getOrderByOrderNo(
@P("订单号,格式如 SO 开头") String orderNo
) {
return orderService.findByOrderNo(orderNo);
}
/** 工具2:查物流 */
@Tool("根据物流单号查询物流轨迹")
public LogisticsInfo getLogisticsByTrackingNo(
@P("物流单号") String trackingNo
) {
return logisticsService.track(trackingNo);
}
/** 工具3:查天气(系列要求的最小示例) */
@Tool("查询指定城市的天气")
public WeatherInfo getWeather(
@P("城市名,如北京、上海") String city
) {
return weatherService.getWeather(city);
}
}
3.3 工具描述的质量决定 Agent 的准确率
// ❌ 差的描述:模型不知道什么时候该用
@Tool("查询")
public OrderInfo getOrder(String no) { ... }
// ✅ 好的描述:模型能精确匹配用户意图
@Tool("根据订单号查询订单的状态、金额和物流信息。当用户询问订单进度、包裹位置、发货状态时使用此工具")
public OrderInfo getOrderByOrderNo(
@P("订单号,格式为 SO 开头加日期序列,如 SO20250912001") String orderNo
) { ... }
工具描述是给 LLM 看的"工具说明书"——描述越精准,模型选工具的准确率越高。三条纪律:
| 纪律 | 说明 |
|---|---|
| 动词+场景 | "当用户询问 X 时使用"比"查询 X"好——告诉模型何时用 |
| 参数格式示例 | "SO 开头如 SO20250912001"比"订单号"好——减少参数幻觉 |
| 一个工具一件事 | 别把查订单+查物流+查天气塞一个方法——粒度越细选择越准 |
四、记忆层:让 Agent 记住上下文
4.1 为什么需要记忆
第一轮:"帮我查 SO20250912001" → Agent 调工具 → 回答订单已发货
第二轮:"物流到哪了" → 没有"SO20250912001"→ 模型不知道查哪个单
→ 有记忆 → 知道"物流"指的是上一轮查的那个订单 → 调查物流工具
没有记忆,每轮对话都是独立的——用户每次都要重复订单号。有记忆,Agent 能理解"它"指代什么。
4.2 短期记忆:ChatMemory
// 短期记忆:最近 N 条对话消息
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.maxMessages(20) // 保留最近 20 条消息(防 context window 超限)
.build();
maxMessages 不是越大越好:每条消息都算 token,20 条消息可能 5K token,模型 context window 有上限。截断策略:保留最近 N 条 + SystemMessage 永不截断——SystemMessage 是 Agent 角色设定,必须在每轮都带上。
4.3 长期记忆:持久化存储
/**
* 长期记忆:按 userId 持久化对话历史
* 用户下次回来能接着聊
*/
@Component
public class PersistentChatMemoryStore implements ChatMemoryStore {
private final ChatMemoryRepository repo; // Redis / PostgreSQL / MongoDB
@Override
public List<ChatMessage> getMessages(Object memoryId) {
String userId = (String) memoryId;
return repo.findByUserId(userId)
.stream()
.map(ChatMemoryEntry::toChatMessage)
.toList();
}
@Override
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
String userId = (String) memoryId;
repo.save(userId, messages); // 每轮对话结束存一次
}
@Override
public void deleteMessages(Object memoryId) {
repo.deleteByUserId((String) memoryId);
}
}
| 记忆类型 | 存什么 | 生命周期 | 存储 |
|---|---|---|---|
| 短期 | 当前会话的对话历史 | 会话结束后可丢 | 内存(ChatMemory) |
| 长期 | 用户身份、偏好、历史订单 | 跨会话保留 | Redis/PG/MongoDB |
| SystemMessage | Agent 角色设定 | 永不截断 | 代码/配置 |
4.4 记忆的三个坑
| 坑 | 说明 | 对策 |
|---|---|---|
| context window 超限 | 对话 50 轮后消息太多,模型报 token 超限 | maxMessages 截断 + 摘要压缩(把前 30 轮摘要成一条消息) |
| 工具结果太大 | 查 DB 返回 100 行数据 → 单条消息 5K token | 工具内部做截断/摘要,只返回模型需要的关键字段 |
| 多用户串记忆 | memoryId 用了固定值 → A 用户的对话 B 也能看到 | memoryId 必须绑定 userId,每个用户独立 ChatMemory |
五、编排层:Agent 主循环
5.1 ReAct 模式
Agent 的核心循环叫 ReAct(Reason + Act):
循环开始:
① Reason(思考):LLM 看到用户问题 + 可用工具 → 决定下一步做什么
② Act(行动):框架执行 LLM 选择的工具
③ Observe(观察):把工具结果喂回 LLM
④ 判断:LLM 决定"够了,回答用户"还是"还需要调更多工具"
→ 如果还要调工具 → 回到 ①
→ 如果可以回答了 → 输出最终回答 → 循环结束
示例(查订单+查物流,两步):
轮1 Reason: 用户要查订单 → 选 getOrderByOrderNo("SO20250912001")
Act: 执行 → 返回 {status:"已发货", trackingNo:"SF1234567"}
Observe: 喂回 LLM → LLM 判断"用户可能还想知道物流详情"
轮2 Reason: 用户问了物流 → 选 getLogisticsByTrackingNo("SF1234567")
Act: 执行 → 返回 {轨迹:[北京→上海], eta:"明天下午"}
Observe: 喂回 LLM → LLM 判断"信息够了"
→ 输出: "您的订单已发货,顺丰单号 SF1234567,预计明天下午送达。当前在北京分拣中心。"
这个循环 LangChain4j 的 AiServices 自动编排——你不需要手写 while 循环,框架在 @AiService 代理里自动做了。
5.2 循环终止条件
终止条件(满足任一即停):
① LLM 输出最终回答(不再选工具)→ 正常结束
② 轮次超过 maxIterations(防死循环)→ 强制结束 + 返回"我处理不了"
③ 工具执行异常超过 maxRetries → 降级返回
maxIterations 是安全阀:LLM 可能在"调工具→结果不对→再调工具→还是不对"的循环里停不下来。设一个上限(如 5 次),超了强制终止——防止单次请求烧掉 $1 的 token。
六、最小可运行 Agent:完整代码
6.1 依赖
<dependencies>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
</dependency>
</dependencies>
6.2 Agent 接口定义
@AiService
public interface CustomerAgent {
@SystemMessage("""
你是一个电商客服助手。你可以:
1. 根据订单号查询订单状态
2. 根据物流单号查询物流轨迹
3. 查询城市天气
规则:
- 当用户问到订单或物流时,主动调用对应工具查询,不要说"我无法查询"
- 如果用户没提供订单号,先问用户要
- 回答要简洁,引用工具返回的真实数据,不要编造
""")
String chat(@MemoryId String userId, // 按用户隔离记忆
@UserMessage String message);
}
6.3 工具实现
@Component
public class AgentTools {
private final OrderService orderService;
private final LogisticsService logisticsService;
private final WeatherService weatherService;
@Tool("根据订单号查询订单状态、金额和物流单号。当用户询问订单进度、包裹位置、发货状态时使用")
public OrderInfo getOrderByOrderNo(
@P("订单号,格式为 SO 开头如 SO20250912001") String orderNo
) {
return orderService.findByOrderNo(orderNo);
}
@Tool("根据物流单号查询物流轨迹和预计送达时间。当用户询问物流详情、快递到哪了时使用")
public LogisticsInfo getLogisticsByTrackingNo(
@P("物流单号,如 SF1234567") String trackingNo
) {
return logisticsService.track(trackingNo);
}
@Tool("查询指定城市的天气。当用户问今天天气、要不要带伞时使用")
public WeatherInfo getWeather(
@P("城市名,如北京、上海、深圳") String city
) {
return weatherService.getWeather(city);
}
}
6.4 记忆配置
@Configuration
public class AgentConfig {
@Bean
public ChatMemoryProvider chatMemoryProvider() {
// 每个 userId 独立的 ChatMemory
return memoryId -> MessageWindowChatMemory.builder()
.id(memoryId)
.maxMessages(20)
.build();
}
@Bean
public CustomerAgent customerAgent(
ChatLanguageModel model,
AgentTools tools,
ChatMemoryProvider memoryProvider
) {
return AiServices.builder(CustomerAgent.class)
.chatLanguageModel(model)
.tools(tools) // 注册工具
.chatMemoryProvider(memoryProvider) // 注入记忆
.build();
}
}
6.5 Controller
@RestController
@RequiredArgsConstructor
public class AgentController {
private final CustomerAgent agent;
@PostMapping("/agent/chat")
public String chat(@RequestBody ChatRequest req) {
// userId 从认证上下文取,不用前端传
String userId = SecurityContextHolder.getContext().getAuthentication().getName();
return agent.chat(userId, req.getMessage());
}
}
record ChatRequest(String message) {}
6.6 运行效果
请求1: POST /agent/chat {"message": "帮我查一下订单 SO20250912001"}
→ Agent 调 getOrderByOrderNo("SO20250912001")
→ 回答: "您的订单 SO20250912001 已发货,金额 299.00 元,物流单号 SF1234567。"
请求2: POST /agent/chat {"message": "物流到哪了"} ← 没传订单号!
→ Agent 从记忆中拿到"物流"指 SF1234567
→ 调 getLogisticsByTrackingNo("SF1234567")
→ 回答: "当前在北京分拣中心,预计明天下午送达。"
请求3: POST /agent/chat {"message": "今天北京天气怎么样"}
→ Agent 调 getWeather("北京")
→ 回答: "北京今天晴,气温 18~25°C。"
请求4: POST /agent/chat {"message": "那我出门拿快递需要带伞吗"}
→ Agent 从记忆中知道"北京晴天"
→ 回答: "北京今天是晴天,不需要带伞。"
请求2 是 Agent 价值的集中体现:用户没说订单号也没说物流单号,但 Agent 靠记忆+推理完成了两跳调用。这比纯聊天 API 多了"记忆"和"行动"两件武器。
七、常见问题
7.1 Agent 选错工具怎么办?
三层排查:① 工具描述不够好——"查询"太模糊,改成"当用户问 X 时使用"的动词+场景描述;② 工具太多太相似——两个工具描述重叠,模型分不清。合并或改描述明确边界;③ 模型能力不够——DeepSeek 在简单场景选工具够用,复杂多步推理可能需要 gpt-4o。排查方法:开 log-requests: true,看 LLM 实际选了哪个工具、传了什么参数——90% 的"选错"是描述问题不是模型问题。
7.2 Agent 一轮调用要多久?成本多少?
单轮 Agent 调用 = 1~N 次 LLM 请求(每轮思考都是一次请求)。典型场景:简单一跳(查天气)= 1 次调用 ~2s ~$0.001;两跳(查订单+查物流)= 3 次调用 ~6s ~$0.003(DeepSeek)。成本公式:每轮 token 数 × 轮数 × 单价。生产必须监控每轮调用的 LLM 次数 + 总 token + 总耗时,设日预算告警——Agent 的成本是聊天 API 的 3~5 倍(因为多次调用)。
7.3 Agent 会无限循环吗?
会,但有两个安全阀:① maxIterations(AiServices 默认有上限)——超过强制终止;② LLM 自身判断——拿到足够信息后会停止选工具、直接输出回答。人为兜底:给 SystemMessage 加一条规则"最多调用 3 次工具,之后直接回答用户"——让模型自己管轮次预算。
7.4 @MemoryId 怎么保证用户隔离?
@MemoryId 的值是 ChatMemory 的 key——同一个值共享记忆,不同值各自独立。生产纪律:MemoryId 必须从认证上下文(JWT/Session)取,不能用前端传的 userId(可伪造)。多租户场景 MemoryId = tenantId:userId,保证租户间也不串记忆。
7.5 Agent 能做多步复杂任务吗(如"帮我退掉上周买的那个订单")?
能但当前最小 Agent 做不了——它缺少任务拆解能力。"退上周的订单"需要:① 查用户最近订单列表(工具)② 找到上周的那个(推理)③ 发起退款(工具)④ 确认退款状态(工具)。这四步需要 Agent 有更强的规划能力——下一篇会讲 Agent 的多步任务编排(Plan-and-Execute 模式)。当前最小 Agent 适合单步/两跳场景,多步场景需要升级编排层。
7.6 Agent 和 RAG 什么关系?要一起用吗?
互补关系,通常一起用:RAG 是 Agent 的一个工具——当用户问"退换货政策是什么"时,Agent 选择 searchKnowledgeBase 工具(RAG 检索),拿到政策文本后组织回答。Agent 负责"决定做什么",RAG 负责"从知识库取信息"。上一篇 RAG 文章里的检索+生成管线,可以作为 Agent 的一个工具嵌入——Agent + RAG = 既能行动又能检索的完整助手。
八、总结
四层架构速查卡
┌──────────┬──────────────────────────────────────────┐
│ 层 │ 关键设计 │
├──────────┼──────────────────────────────────────────┤
│ 模型层 │ Temperature=0.2;Function Calling 必须支持 │
│ 工具层 │ @Tool 描述=动词+场景;参数带格式示例;一工具一事│
│ 记忆层 │ ChatMemory 短期(maxMessages) + 持久化长期 │
│ │ @MemoryId 用户隔离;SystemMessage 永不截断 │
│ 编排层 │ ReAct 循环(思考→行动→观察→回答) │
│ │ AiServices 自动编排;maxIterations 防死循环 │
└──────────┴──────────────────────────────────────────┘
一句话
AI Agent 的本质不是更聪明的聊天,而是 LLM+工具+记忆+规划的四层组合:LLM 是大脑理解意图选择工具,@Tool 注解的手脚执行实际动作,ChatMemory 的记忆保持多轮对话上下文,ReAct 循环编排"思考→行动→观察→回答"的完整闭环。LangChain4j 用 @AiService 把这四层藏在一个声明式接口背后——你只写接口和工具,框架自动跑循环、选工具、管记忆、喂结果。最小可运行 Agent 的代码量不超过 100 行,但它已经能做纯聊天 API 做不到的事:在用户没说订单号时靠记忆查物流,在需要行动时主动调数据库而不是说"我无法查询"。这就是 Agent 和聊天的分水岭——不是更会说,而是能做事。
给团队的建议
| 项 | 建议 |
|---|---|
| 起步 | 用本文最小 Agent 验证链路(3 个工具 + ChatMemory + DeepSeek) |
| 工具 | @Tool 描述花时间打磨——它是 Agent 准确率的第一杠杆 |
| 记忆 | @MemoryId 绑认证上下文;maxMessages=20 防超限 |
| 成本 | 每轮调用监控 LLM 次数 + token + 耗时;设日预算告警 |
| 安全 | 工具内部做权限校验——LLM 可能被诱导调不该调的工具(提示词注入) |
| 演进 | 单跳→多跳→多步规划(下一篇),不要一步到位做复杂 Agent |
互动话题:你们做的 AI 应用是"聊天 API"还是"Agent"?有没有遇到过"模型说不了"但其实加个工具就能解决的场景?评论区聊聊。
参考资料
- LangChain4j 官方文档:AI Services
- LangChain4j 官方文档:Tools(Function Calling)
- LangChain4j 官方文档:Chat Memory
- ReAct 论文:Reason+Act(Yao et al., 2022)
- OpenAI Function Calling 文档
- DeepSeek API 文档(OpenAI 兼容)
- LangChain4j GitHub 示例
标题:从0搭建你的第一个 AI Agent:LangChain4j 架构总览+最小可运行示例
作者:jiangyi
地址:http://jiangyi.space/articles/2026/09/16/1789202418174.html
公众号:服务端技术精选
- 引言
- 一、Agent 的本质:不是聊天,是行动
- 1.1 聊天 vs Agent 的区别
- 1.2 四层架构
- 二、模型层:LLM 是大脑
- 2.1 Agent 为什么要用 LLM
- 2.2 模型选型
- 三、工具层:Function Calling 是手脚
- 3.1 工具的本质
- 3.2 @Tool 注解定义工具
- 3.3 工具描述的质量决定 Agent 的准确率
- 四、记忆层:让 Agent 记住上下文
- 4.1 为什么需要记忆
- 4.2 短期记忆:ChatMemory
- 4.3 长期记忆:持久化存储
- 4.4 记忆的三个坑
- 五、编排层:Agent 主循环
- 5.1 ReAct 模式
- 5.2 循环终止条件
- 六、最小可运行 Agent:完整代码
- 6.1 依赖
- 6.2 Agent 接口定义
- 6.3 工具实现
- 6.4 记忆配置
- 6.5 Controller
- 6.6 运行效果
- 七、常见问题
- 7.1 Agent 选错工具怎么办?
- 7.2 Agent 一轮调用要多久?成本多少?
- 7.3 Agent 会无限循环吗?
- 7.4 @MemoryId 怎么保证用户隔离?
- 7.5 Agent 能做多步复杂任务吗(如"帮我退掉上周买的那个订单")?
- 7.6 Agent 和 RAG 什么关系?要一起用吗?
- 八、总结
- 四层架构速查卡
- 一句话
- 给团队的建议
- 参考资料
评论