LangChain4j + DeepSeek:Java 开发者构建第一个 Agent 的完整指南
不用 Python不用 LangChain用你最熟悉的 Spring Boot 和 Java 21从零构建一个能自动调用工具的 AI Agent。为什么是 LangChain4j提到 AI Agent 开发Python LangChain 几乎成了标准答案。但对于 Java 后端开发者来说这条路有 3 个痛点语言切换成本——团队要用两套技术栈Agent 写 Python后端写 Java集成摩擦——Agent 通过 REST/gRPC 调用后端服务增加延迟和维护负担生态割裂——Spring Boot 的依赖注入、配置管理、监控在 Python 侧全部重来一遍LangChain4j 解决了这个问题。它是 LangChain 的 Java 移植但又不仅仅是移植——它充分利用了 Java 生态的优势注解驱动的 Tool 声明、Spring Boot 自动配置、强类型的 AiServices 接口。本文带你从零开始用 LangChain4j DeepSeek Spring Boot 构建一个完整的工业设备诊断 Agent。前置准备依赖版本说明JDK21推荐 Corretto 21Spring Boot3.3.03.3.0LangChain4j0.35.0Java Agent 框架DeepSeek API Key-注册即送额度Docker-运行 EMQX 消息中间件可选—Step 1项目骨架创建一个标准的 Spring Boot 项目pom.xml核心依赖parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.3.0/version /parent dependencies dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version0.35.0/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId version0.35.0/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency /dependencies两个关键点langchain4j提供 Agent 核心能力AiServices、Tool 注解、ChatMemorylangchain4j-open-ai提供 OpenAI 兼容的 LLM 客户端——DeepSeek 的 API 是 OpenAI 格式的所以直接用这个模块Step 2配置 LLM 连接application.ymllangchain4j: open-ai: chat-model: base-url: https://api.deepseek.com api-key: ${DEEPSEEK_API_KEY:} model-name: deepseek-chat temperature: 0.3 max-tokens: 2048 log-requests: true log-responses: true几个值得解释的选择temperature0.3工业诊断场景需要确定性——你不能让 Agent 面对同一个设备告警一次说”轴承磨损”一次说”可能是电源问题”。0.3 是在推理能力和确定性之间的平衡点。max-tokens2048单次诊断回复加上上下文2048 足够。多出来的 token 就是多出来的钱。log-requests: true这是调试 Agent 最关键的一行配置。打开之后你会在控制台看到完整的请求体包括发给 LLM 的工具定义 JSON Schema和响应体包括 LLM 返回的 tool_calls。安全存储 API Key不要直接把 key 写在 application.yml 里。用application-local.yml加入.gitignorelangchain4j: open-ai: chat-model: api-key: sk-your-real-key主配置文件里用${DEEPSEEK_API_KEY:}读环境变量local 文件覆盖。这样 git push 不会泄露。Java 配置类Configuration public class AgentConfig { Value(${langchain4j.open-ai.chat-model.base-url}) private String baseUrl; Value(${langchain4j.open-ai.chat-model.api-key}) private String apiKey; Value(${langchain4j.open-ai.chat-model.model-name}) private String modelName; Value(${langchain4j.open-ai.chat-model.temperature}) private Double temperature; Value(${langchain4j.open-ai.chat-model.max-tokens}) private Integer maxTokens; Value(${langchain4j.open-ai.chat-model.log-requests}) private Boolean logRequests; Value(${langchain4j.open-ai.chat-model.log-responses}) private Boolean logResponses; Bean public OpenAiChatModel chatModel() { return OpenAiChatModel.builder() .baseUrl(baseUrl) .apiKey(apiKey) .modelName(modelName) .temperature(temperature) .maxTokens(maxTokens) .logRequests(logRequests) .logResponses(logResponses) .timeout(Duration.ofSeconds(60)) .build(); } Bean public ChatMemory chatMemory() { return MessageWindowChatMemory.withMaxMessages(20); } }两个 BeanBean作用OpenAiChatModelLLM 客户端指向 DeepSeek APIChatMemory对话记忆保留最近 20 轮消息注意手动构建OpenAiChatModelBean 时log-requests和log-responses不会自动从 yml 读取。必须显式Value注入后.logRequests(logRequests)传给 builder。只写 yml 不写 builder 调用是不生效的——这是本人在排查日志缺失时踩的坑。MessageWindowChatMemory.withMaxMessages(20)的含义Agent 会记住最近 20 条消息用户消息 AI 回复 工具调用结果超出窗口的自动丢弃。20 是一个经验值——足够覆盖一次完整的诊断对话”查告警 → 查数据 → 诊断 → 出建议”又不会让 token 消耗失控。Step 3写第一个 ToolTool 是 Agent 的”手”。LLM 只能思考和生成文本但有了 Tool它就能查询数据库、调用 API、操作设备。LangChain4j 的 Tool 声明极其简洁——在方法上加Tool注解注解里的字符串就是给 LLM 看的工具描述Component public class DeviceAlarmTool { Tool(查询指定设备的当前告警信息。输入设备ID返回该设备的所有活跃告警。) public String queryDeviceAlarms(String deviceId) { // 这里查询 TDEngine / InfluxDB / 告警平台 // 返回 JSON 格式的告警列表 } }Tool描述是 Agent 的大脑提示。LLM 会根据这个描述在对话中自动判断”用户这句话需要调用这个工具吗”。你不需要写意图识别、if-else 路由、参数抽取——LLM 自己搞定。Tool 设计的核心原则描述要具体。不是”查询信息”而是”查询指定设备的当前告警信息。输入设备ID返回该设备的所有活跃告警。”LLM 需要足够的上下文来判断什么时候该用这个工具。入参用简单类型。String、int、double 是最安全的。LLM 是从自然语言中提取参数值的简单的参数类型让提取更准确。返回 JSON 字符串。LLM 最擅长处理 JSON。返回结构化的 JSON 让它能提取关键字段做下一步推理。方法名即语义。queryDeviceAlarms比getInfo好 10 倍。LLM 会使用方法名和 Tool 描述来判断工具的用途。Step 4组装 Agent这是 LangChain4j 最精彩的部分——AiServices.builder()Service RequiredArgsConstructor public class DeviceAgent { private final OpenAiChatModel chatModel; private final ChatMemory chatMemory; private final DeviceAlarmTool alarmTool; private final DeviceDataTool dataTool; private final DiagnosisTool diagnosisTool; public String chat(String userMessage) { IndustrialAssistant assistant AiServices.builder(IndustrialAssistant.class) .chatLanguageModel(chatModel) .chatMemory(chatMemory) .tools(alarmTool, dataTool, diagnosisTool) .build(); return assistant.chat(userMessage); } interface IndustrialAssistant { String chat(String message); } }发生了什么AiServices.builder(IndustrialAssistant.class)传入一个接口框架用动态代理自动生成实现类。.tools(alarmTool, dataTool, diagnosisTool)注册 3 个工具框架会自动提取 Tool 注解的描述转成 OpenAI Function Calling 的 JSON Schema。.build()生成代理实例。你调assistant.chat(userMessage)时底层执行流程是用户消息 ↓ ChatMemory加载历史对话 ↓ OpenAI Chat API消息 工具定义 JSON Schema ↓ LLM 判断需要调用工具吗 ├── 不需要 → 直接返回回复 └── 需要 → 返回 tool_call {name, arguments} ↓ 框架自动调用对应 Java 方法 ↓ 工具结果发回 LLM ↓ LLM 根据结果生成最终回复整个过程对开发者透明。你不需要写一行 JSON 解析、工具路由、结果拼接的逻辑。Step 5暴露 REST APIRestController RequestMapping(/api/agent) public class AgentController { private final DeviceAgent agent; public AgentController(DeviceAgent agent) { this.agent agent; } PostMapping(/chat) public ResponseEntityMapString, String chat(RequestBody ChatRequest request) { String reply agent.chat(request.message()); return ResponseEntity.ok(Map.of(reply, reply)); } public record ChatRequest(String message) {} }Step 6测试启动应用# 先启动 MQTT可选Agent 不依赖它也能跑 docker compose up -d # 设置 API Key export DEEPSEEK_API_KEYsk-your-key # 启动 ./mvnw spring-boot:run单工具调用——Agent 自动识别意图curl -X POST http://localhost:8080/api/agent/chat \ -H Content-Type: application/json \ -d {message: CNC-001 现在有什么告警}LLM 自动判断”用户想查告警” → 调用queryDeviceAlarms(CNC-001)→ 返回结构化结果。多工具串联——一句话触发三个工具curl -X POST http://localhost:8080/api/agent/chat \ -H Content-Type: application/json \ -d {message: CNC-001 刚报了振动异常告警查一下最近数据帮我诊断。}Agent 的执行链路1. LLM 理解意图 → 需要告警 数据 2. 调用 queryDeviceAlarms(CNC-001) → 告警信息 3. 调用 queryDeviceHistory(CNC-001) → 遥测数据 4. LLM 分析结果 → 振动超标需要诊断 5. 调用 generateDiagnosis(振动异常, vibration4.8) 6. LLM 整合 → 完整诊断报告这才是 AI Agent 的本质不是预设的工作流 DAG而是 LLM 的动态推理——它自己决定先做什么、后做什么、什么时候信息够了。Step 7调试Agent 调试最大的痛点是”黑盒”——你调chat()得到一个回复但 LLM 中间调用了哪些工具、传了什么参数、返回了什么结果全看不见。开启日志需要两步配合1. 开启 LangChain4j 的请求/响应拦截# application.yml langchain4j: open-ai: chat-model: log-requests: true log-responses: true并确保AgentConfig中显式传给了 builder见前面 Step 2 的代码。2. 开启 openai4j 客户端的 DEBUG 日志# application.yml logging: level: dev.ai4j.openai4j: DEBUGLangChain4j 底层使用dev.ai4j.openai4j这个 HTTP 客户端与 DeepSeek 通信实际的日志输出是 OkHttp 拦截器发出的logger 名是dev.ai4j.openai4j.RequestLoggingInterceptor。如果你只配了logging.level.dev.langchain4j: DEBUG是看不到这些日志的——包名不同。实际日志输出启动后会看到类似这样的日志body 是单行 JSON非格式化DEBUG dev.ai4j.openai4j.RequestLoggingInterceptor - Request: - method: POST - url: https://api.deepseek.com/v1/chat/completions - headers: [Content-Type: application/json, Authorization: Bearer sk-...xx] - body: {model:deepseek-chat,messages:[{role:user,content:CNC-001 有什么告警}],tools:[{type:function,function:{name:queryDeviceAlarms,description:查询指定设备的当前告警信息...,parameters:{type:object,properties:{deviceId:{type:string}}}}}],temperature:0.3,max_tokens:2048} DEBUG dev.ai4j.openai4j.ResponseLoggingInterceptor - Response: - status code: 200 - headers: [Content-Type: application/json, ...] - body: {id:chatcmpl-xxx,choices:[{message:{tool_calls:[{function:{name:queryDeviceAlarms,arguments:{\deviceId\:\CNC-001\}}}]}}]}一眼能看到LLM 收到了哪些工具描述、选择了哪个工具、传了什么参数。单行 JSON 不太好看但调试效率已经提升了 10 倍。如果需要格式化查看可以把 body 复制到任意 JSON 格式化工具中。常见踩坑1. 401 鉴权失败DeepSeek API 是 OpenAI 兼容的但api-key不能为空或占位符。确认DEEPSEEK_API_KEY环境变量已设置或application-local.yml中有正确值。2. Tool 不被调用检查 Tool 注解的 import——必须是dev.langchain4j.agent.tool.Tool不是其他包的同名注解。另外描述不能太笼统LLM 需要足够的语义信息来判断”什么时候该用这个工具”。3. Memory 丢失MessageWindowChatMemory是在 JVM 内存里的服务重启就丢。如果需要在重启后保持对话记忆换成持久化的实现如 Redis 或数据库或使用ChatMemoryStore接口自定义存储。4. 开了 log-requests 但看不到日志yml 里配了log-requests: true也传给了 builder但控制台一条日志都没有根因实际的日志输出由底层 HTTP 客户端dev.ai4j.openai4j的 OkHttp 拦截器产生而不是dev.langchain4j包。日志级别是 DEBUG。修复logging: level: dev.ai4j.openai4j: DEBUG5. 每次 chat() 都 new 一个 AiServices注意代码里每次chat()都调了AiServices.builder().build()生成一个新代理。这是有意为之——Memory 是单例 Bean同一个实例被所有请求共享所以对话历史不会丢。每次 rebuild 的开销极小只是动态代理创建可以接受。如果要极致性能可以把IndustrialAssistant也做成 Bean只在构造时 build 一次。下一步这篇指南覆盖了一个可运行的 Agent 从零到一的全部步骤。但这只是一个起点——模拟数据、单 Agent、无 RAG、无评估。接下来的文章会逐步深入Agent 工具设计的 5 个原则——如何写出好的 Tool排查 Function Calling 的 4 个常见坑——工具没被调用参数不对ChatMemory 三种策略对比——什么时候用哪种