1. 项目概述一句话背后的真实工作流“让大模型自动查数据库发邮件一句话搞定复杂任务”——这句话在最近的开发者群和内部技术分享会上被反复提起但真正跑通它的人其实不多。我带过几个模拟项目X的实操训练发现超过七成的参与者卡在“工具调用链路断裂”上不是大模型根本没意识到该调哪个函数就是参数传进去后数据库返回空或者邮件模板里变量渲染失败。问题不在于LangChain4j本身难而在于它把“意图识别→工具选择→参数提取→执行→结果整合”这整条链路全交给了开发者自己缝合中间任何一个环节松动整个自动化就垮了。这个项目标题里的关键词——LangChain4j、Tool Calling、查数据库、发邮件——不是并列关系而是有严格时序依赖的四层动作先用Tool Calling机制把用户自然语言中的结构化意图拆解出来再通过预定义的工具描述ToolSpecification让模型知道“查数据库”对应的是哪个Java方法接着把提取出的参数比如“张三的订单号”安全注入到JDBC查询中最后把查到的结果喂进邮件模板引擎触发SMTP发送。它解决的不是“能不能做”而是“怎么让大模型在无人干预下稳定走完这四步闭环”。适合两类人一类是已有Spring Boot后端系统、想快速给业务接口加AI入口的工程师另一类是正在搭建智能客服或运营助手的团队需要把“查订单发通知”这类高频组合操作从人工流程里彻底抽离出来。它不依赖GPU不训练模型纯靠工程设计把LLM变成一个可编程的“业务协作者”。我试过三种主流实现路径纯Prompt Engineering硬编码工具名、用Function Calling API做轻量封装、以及本项目采用的LangChain4j原生Tool Calling。前两种要么维护成本高每次加个新工具就得改Prompt要么对参数类型校验弱传错字符串类型导致SQL报错。LangChain4j的优势在于它把工具注册、参数解析、执行异常捕获、结果格式化全部标准化了你只需要专注写好每个工具的Java方法体——就像写一个普通Service一样。但代价是你必须吃透它的ToolExecutor调度逻辑、ToolSpecification字段语义、以及ToolResult如何被LLM重新理解。这不是一个“引入依赖就能跑”的玩具而是一套需要你亲手拧紧每一颗螺丝的自动化流水线。2. 整体架构与设计思路为什么选LangChain4j而不是自己造轮子2.1 四层职责划分从用户输入到邮件发出的完整链路整个系统不是单点突破而是分层解耦的四个责任模块每层只做一件事且边界清晰意图理解层由LLM如Qwen2-7B本地部署版完成。它不直接操作数据只负责把“帮我查张三昨天下的订单发邮件告诉他发货了”这句话精准映射到两个已注册工具上queryOrderService和sendEmailService并提取出customerName张三、date2024-05-20等参数。关键点在于LLM输出的不是自由文本而是严格遵循OpenAI Function Calling格式的JSON对象LangChain4j会自动解析这个JSON并匹配工具。工具注册层这是LangChain4j区别于其他框架的核心。你不是写一个通用API网关而是为每个业务动作明确定义一个Tool接口实现类。比如QueryOrderTool类里execute(MapString, Object parameters)方法体就是一段标准JDBC代码SendEmailTool里则封装了JavaMailSender的调用逻辑。每个工具必须配套一个ToolSpecification它像一份“工具说明书”告诉LLM“我叫queryOrder需要customerName字符串和date日期格式yyyy-MM-dd返回值是订单列表JSON”。这个说明书不是给人看的是给LLM读的——它决定了模型能否正确生成参数。执行调度层ToolExecutor是真正的指挥官。它接收LLM输出的工具调用请求根据ToolSpecification做三件事① 校验参数类型是否匹配比如date字段传了2024/05/20就会被拦截② 把参数Map安全转换成Java对象避免反射漏洞③ 调用对应工具的execute()方法并捕获所有异常。如果某次查询数据库超时它不会让整个对话崩掉而是把异常信息包装成ToolResult返回给LLM模型可能据此生成“抱歉订单系统暂时繁忙请稍后再试”。结果整合层LLM第二次调用时会收到前序工具的执行结果比如{orderNo:ORD20240520001,status:shipped}它要做的不是再执行一次而是把这段JSON自然语言化填进预设的邮件模板“尊敬的张三您的订单ORD20240520001已发货……”最后调用sendEmailService发出。这里的关键是LLM必须理解ToolResult是“已完成动作的输出”而不是“待处理的新输入”。提示很多初学者误以为Tool Calling是让LLM直接执行SQL这是危险误区。LangChain4j的设计哲学是“LLM只决策不执行”。所有数据库连接、邮件发送、文件IO等敏感操作必须封装在Java工具方法里由Spring容器管理事务和连接池。LLM输出的永远是工具名和参数执行权牢牢掌握在你的代码手里。2.2 为什么不用Spring AI或自研HTTP网关对比过Spring AI的AiModel抽象和自建RESTful工具网关LangChain4j的胜出点非常实际参数强类型校验不可替代Spring AI的FunctionCallback只提供MapString, Object你得自己写if (params.get(date) instanceof String)做类型判断而LangChain4j的ToolSpecification在注册时就声明了ParameterSpec.of(date, string, 订单日期格式yyyy-MM-dd, true)框架会在执行前自动校验。我遇到过真实案例前端传来的date是2024-05-20T00:00:00JDBC PreparedStatement直接报错而LangChain4j的校验能提前拦截并返回友好提示。异常处理粒度更细自建网关通常把所有异常统一转成500但LangChain4j允许你在工具方法里抛出ToolExecutionException并携带业务码如ORDER_NOT_FOUNDLLM能据此生成差异化回复“没找到张三的订单” vs “系统错误请重试”。这种能力在客服场景里价值巨大。调试可视化友好LangChain4j的ToolExecutionRequest和ToolExecutionResult对象天然支持日志打印你可以清晰看到“LLM第几次调用、调了哪个工具、传了什么参数、返回了什么结果”。而HTTP网关的日志里只有request/response中间少了LLM的决策过程排查“为什么没调用发邮件工具”这种问题时LangChain4j的日志能直接定位到是ToolSpecification描述不够清晰还是LLM理解偏差。当然它也有代价学习曲线比调一个HTTP接口陡峭。你需要理解ToolSpecification的description字段怎么写才能让LLM准确识别太笼统会误调太具体又缺乏泛化性还要处理ToolResult的序列化问题默认用Jackson如果返回自定义对象需加JsonSerialize。但这些“麻烦”恰恰是生产环境稳定性的基石——它们强迫你把模糊的业务逻辑变成可验证、可测试、可审计的代码契约。2.3 架构图不是画出来的是跑出来的不要被“架构图”三个字吓住。这个系统的物理结构其实就三样东西一个Spring Boot Web应用含LLM客户端、一个MySQL数据库、一个SMTP邮件服务器。所谓的“架构”是你在application.yml里配的几行langchain4j: tool-executor: max-retries: 2 # 工具执行失败最多重试2次 timeout: 30s # 单次工具执行超时30秒 model: provider: qwen # 使用Qwen模型 base-url: http://localhost:8000/v1 api-key: sk-xxx真正的架构体现在代码组织上。我坚持把工具类放在com.example.tools包下每个工具对应一个独立的Component比如QueryOrderTool.java和SendEmailTool.java。它们不继承任何特殊父类只是实现了Tool接口。这样做的好处是你可以像测试普通Service一样用JUnit单独测试QueryOrderTool.execute()传入{customerName:张三}断言返回的订单列表是否非空。这种可测试性是架构健壮性的第一道防线。3. 核心细节解析与实操要点从零开始搭起这条流水线3.1 工具注册ToolSpecification的每一个字段都在说话ToolSpecification不是配置项它是LLM的“操作手册”。它的五个核心字段每个都直接影响模型能否正确调用name工具唯一标识符必须是小写字母下划线比如query_order。注意不能用驼峰queryOrderLLM在输出JSON时会严格匹配这个字符串。我踩过的坑是早期用了queryOrder结果LLM输出{name:queryOrder}而框架注册的是query_order匹配失败整个调用静默跳过。description这是最关键的字段长度建议控制在120字符内。它不是写给程序员看的而是教LLM“什么时候该用你”。比如query_order的描述不能写成“查询订单表”而要写成“根据客户姓名和日期查询订单信息用于核实发货状态”。这里包含了使用场景核实发货、输入要素客户姓名、日期、业务目的不是单纯查数据而是为后续动作服务。我实测过把描述改成“查询客户订单”LLM在用户说“查张三的订单”时能调用但说“看看张三昨天买了啥”就失效了——因为“买了啥”没在描述里体现。parameters参数列表每个ParameterSpec必须明确三点name参数名如customerName、type类型string/integer/boolean/array、description用途说明。特别注意type如果数据库字段是DATE类型这里必须写string因为LLM输出的永远是字符串框架会负责转换。别写dateLangChain4j没有这个类型。description要包含格式要求比如订单日期格式yyyy-MM-dd否则LLM可能输出2024年5月20日。requiredParameters必填参数数组写[customerName]表示customerName不能为空。如果用户只说“查订单”没提人名LLM会主动追问而不是传null进去导致SQL报错。returnDescription返回值说明比如订单信息JSON数组包含orderNo、status、amount字段。这决定了LLM能否正确理解结果并用于下一步。如果这里写“订单数据”LLM可能忽略status字段直接把整个JSON当字符串塞进邮件。下面是一个生产环境可用的QueryOrderTool注册代码Component public class QueryOrderTool implements Tool { private final JdbcTemplate jdbcTemplate; public QueryOrderTool(JdbcTemplate jdbcTemplate) { this.jdbcTemplate jdbcTemplate; } Override public ToolSpecification specification() { return ToolSpecification.builder() .name(query_order) .description(根据客户姓名和日期查询订单信息用于核实发货状态) .parameters( ParameterSpec.builder() .name(customerName) .type(string) .description(客户姓名必须完全匹配数据库记录) .required(true) .build(), ParameterSpec.builder() .name(date) .type(string) .description(订单日期格式yyyy-MM-dd例如2024-05-20) .required(true) .build() ) .returnDescription(订单信息JSON数组包含orderNo订单号、status状态created/shipped/cancelled、amount金额字段) .build(); } Override public ToolResult execute(MapString, Object parameters) { String customerName (String) parameters.get(customerName); String dateStr (String) parameters.get(date); // 安全校验防止SQL注入只允许字母数字和中文 if (!customerName.matches([\\u4e00-\\u9fa5a-zA-Z0-9])) { throw new ToolExecutionException(customerName包含非法字符); } // 日期格式校验 LocalDate date; try { date LocalDate.parse(dateStr, DateTimeFormatter.ofPattern(yyyy-MM-dd)); } catch (DateTimeParseException e) { throw new ToolExecutionException(date格式错误应为yyyy-MM-dd); } String sql SELECT order_no as orderNo, status, amount FROM orders WHERE customer_name ? AND DATE(create_time) ?; ListMapString, Object results jdbcTemplate.queryForList(sql, customerName, date); return ToolResult.of(results); // 自动序列化为JSON字符串 } }注意ToolResult.of(results)不是简单toString()它会用Jackson把List转成标准JSON字符串。如果你返回自定义对象记得加JsonInclude(JsonInclude.Include.NON_NULL)避免空字段污染LLM理解。3.2 LLM集成本地Qwen模型的轻量级接入方案不用买GPU不用租云服务用一台16G内存的MacBook Pro就能跑通全流程。关键是选对模型和客户端模型选择Qwen2-7B-Instruct是当前开源模型里Tool Calling能力最强的之一。它在HuggingFace的tool-alpaca评测集上得分比Llama3-8B高12%尤其擅长多工具串联。下载地址是Qwen/Qwen2-7B-Instruct量化版本用AWQ格式qwen2-7b-instruct-q4_awq显存占用从14GB降到6GB。本地部署用llama.cpp最省事。编译后一行命令启动./main -m ./models/qwen2-7b-instruct-q4_awq.gguf -c 2048 --port 8000 --host 0.0.0.0它会暴露一个兼容OpenAI API的/v1/chat/completions端点。LangChain4j开箱即用只需在application.yml里配好base-url。关键参数调优Qwen对temperature0.3最稳定太高0.7会导致工具名拼错query_order变query_orde太低0.1又缺乏灵活性。maxTokens设为1024足够因为Tool Calling输出的JSON很紧凑。stop序列必须加[|eot_id|]这是Qwen的结束标记漏掉会导致响应流卡死。安全加固在application.yml里禁用stream: true因为LangChain4j的Tool Calling需要完整响应才能解析JSON。同时用Spring的RequestHeader(X-API-Key)校验请求头避免未授权访问。3.3 邮件模板让LLM生成的文本可读、可维护、可审计发邮件不是把ToolResult直接贴进去而是用Freemarker做二次加工。原因有三格式控制ToolResult是纯JSON但邮件需要段落、加粗、换行。比如订单状态shipped要显示为“✅ 已发货”这得在模板里写#if order.status shipped✅ 已发货/#if。内容审计所有邮件正文必须经过模板引擎这样你可以在email.ftl里统一加公司水印、法律声明、退订链接。如果LLM直接生成HTML这些合规要素就失控了。多语言支持模板里用#if locale zh发货成功/#if比让LLM自己翻译更可靠。SendEmailTool的代码示例Component public class SendEmailTool implements Tool { private final JavaMailSender mailSender; private final Configuration freemarkerConfig; public SendEmailTool(JavaMailSender mailSender, Configuration freemarkerConfig) { this.mailSender mailSender; this.freemarkerConfig freemarkerConfig; } Override public ToolSpecification specification() { return ToolSpecification.builder() .name(send_email) .description(向指定邮箱发送通知邮件内容基于订单查询结果生成) .parameters( ParameterSpec.builder() .name(toEmail) .type(string) .description(收件人邮箱地址) .required(true) .build(), ParameterSpec.builder() .name(orderData) .type(string) .description(订单查询结果的JSON字符串将用于填充邮件模板) .required(true) .build() ) .returnDescription(邮件发送成功或失败的状态信息) .build(); } Override public ToolResult execute(MapString, Object parameters) { String toEmail (String) parameters.get(toEmail); String orderJson (String) parameters.get(orderData); try { // 解析JSON为Map供Freemarker使用 ObjectMapper mapper new ObjectMapper(); MapString, Object orderMap mapper.readValue(orderJson, Map.class); // 加载模板 Template template freemarkerConfig.getTemplate(email.ftl); StringWriter writer new StringWriter(); template.process(Map.of(order, orderMap, now, new Date()), writer); // 发送邮件 MimeMessage message mailSender.createMimeMessage(); MimeMessageHelper helper new MimeMessageHelper(message, true, UTF-8); helper.setTo(toEmail); helper.setSubject(订单发货通知 - orderMap.get(orderNo)); helper.setText(writer.toString(), true); // true表示HTML mailSender.send(message); return ToolResult.of(邮件已发送至 toEmail); } catch (Exception e) { throw new ToolExecutionException(邮件发送失败: e.getMessage()); } } }email.ftl模板片段p尊敬的客户/p p您的订单 strong${order.orderNo!}/strong 已于 ${.now?string(yyyy年MM月dd日)} 发货。/p table border1 trth订单号/thtd${order.orderNo!}/td/tr trth状态/thtd #if order.status shipped✅ 已发货/#if #if order.status created⏳ 待处理/#if /td/tr trth金额/thtd¥${order.amount!}/td/tr /table p感谢您的信任/p实操心得模板里所有${...!}都加了!默认值操作符比如${order.orderNo!}意思是如果orderNo为null就显示空字符串。这能防止LLM返回的JSON缺字段时模板渲染直接报错。这是线上环境必须加的保险丝。4. 实操过程与核心环节实现手把手跑通第一个“查发”任务4.1 环境准备5分钟搭好最小可运行系统不需要Docker不需要K8s四步搞定本地开发环境数据库初始化用MySQL 8.0执行建表SQLCREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, customer_name VARCHAR(64) NOT NULL, status ENUM(created,shipped,cancelled) DEFAULT created, amount DECIMAL(10,2) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_customer_date (customer_name, create_time) ); INSERT INTO orders (order_no, customer_name, status, amount, create_time) VALUES (ORD20240520001, 张三, shipped, 299.00, 2024-05-20 10:30:00);Spring Boot项目创建用 https://start.spring.io 勾选Spring Web、Spring JDBC、Spring Mail、Lombok。添加LangChain4j依赖dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-spring-boot-starter/artifactId version0.32.0/version /dependencyQwen本地服务启动按前文llama.cpp命令启动确认curl http://localhost:8000/health返回{status:ok}。邮件服务配置在application.yml里配好SMTPspring: mail: host: smtp.qq.com port: 587 username: yourqq.com password: your_app_password # 注意不是邮箱密码是QQ邮箱的SMTP专用密码 properties: mail: smtp: auth: true starttls: enable: true此时项目结构长这样src/main/java/com/example/ ├── DemoApplication.java ├── tools/ │ ├── QueryOrderTool.java │ └── SendEmailTool.java ├── config/ │ └── FreemarkerConfig.java # 配置Freemarker加载email.ftl └── controller/ └── AiController.java # 提供/api/chat接口4.2 核心控制器AiController的三次迭代最初的AiController只有10行但它暴露了所有典型问题RestController public class AiController { private final AiServices aiServices; public AiController(AiServices aiServices) { this.aiServices aiServices; } PostMapping(/api/chat) public String chat(RequestBody String userMessage) { return aiServices.chat(userMessage).content(); } }第一次迭代加入工具调用支持PostMapping(/api/chat) public ResponseEntityMapString, String chat(RequestBody MapString, String request) { String userMessage request.get(message); // 关键启用Tool Calling AiMessage aiMessage aiServices.chat(userMessage) .toolExecutor(toolExecutor()) // 注入自定义ToolExecutor .build(); return ResponseEntity.ok(Map.of(response, aiMessage.content())); }但这时会报错ToolExecutor找不到。因为LangChain4j默认不启用工具执行需要手动注册BeanBean public ToolExecutor toolExecutor() { return ToolExecutor.builder() .tools(List.of( applicationContext.getBean(QueryOrderTool.class), applicationContext.getBean(SendEmailTool.class) )) .build(); }第二次迭代处理多轮工具调用用户一句话触发多个工具LLM可能分两次响应第一次返回{name:query_order,parameters:{customerName:张三,date:2024-05-20}}第二次才返回{name:send_email,parameters:{toEmail:zhangsanexample.com,orderData:{...}}}。所以控制器要能处理AiMessage的toolExecutionRequests()PostMapping(/api/chat) public ResponseEntityMapString, Object chat(RequestBody MapString, String request) { String userMessage request.get(message); ChatMemory chatMemory InMemoryChatMemory.withMaxMessages(10); // 保持上下文 // 第一次调用LLM决定调什么工具 AiMessage firstResponse aiServices.chat(userMessage) .chatMemory(chatMemory) .toolExecutor(toolExecutor()) .build(); // 检查是否有工具调用请求 if (!firstResponse.toolExecutionRequests().isEmpty()) { // 执行所有请求 ListToolExecutionResult results toolExecutor().execute(firstResponse.toolExecutionRequests()); // 第二次调用LLM整合结果并生成最终回复 AiMessage secondResponse aiServices.chat(userMessage) .chatMemory(chatMemory) .toolResults(results) // 把执行结果喂回去 .build(); return ResponseEntity.ok(Map.of( response, secondResponse.content(), toolCalls, firstResponse.toolExecutionRequests().stream() .map(req - req.name() ( req.parameters() )) .collect(Collectors.toList()) )); } return ResponseEntity.ok(Map.of(response, firstResponse.content())); }第三次迭代加入错误熔断和用户友好提示生产环境不能让LLM的错误直接暴露给用户。比如数据库连不上应该返回“订单系统暂时不可用”而不是堆栈日志。在toolExecutor()里加一层包装Bean public ToolExecutor toolExecutor() { return ToolExecutor.builder() .tools(List.of( applicationContext.getBean(QueryOrderTool.class), applicationContext.getBean(SendEmailTool.class) )) .onError((request, error) - { // 统一错误处理 if (error instanceof SQLException) { return ToolResult.of(订单系统暂时不可用请稍后再试); } else if (error instanceof MessagingException) { return ToolResult.of(邮件发送失败请检查邮箱地址是否正确); } else { return ToolResult.of(系统内部错误请联系管理员); } }) .build(); }4.3 真实请求与响应抓包看懂每一步发生了什么用curl发一个真实请求观察整个链路curl -X POST http://localhost:8080/api/chat \ -H Content-Type: application/json \ -d {message:帮我查张三今天下的订单发邮件告诉他发货了收件人zhangsanexample.com}第一阶段LLM输出工具调用请求{ response: , toolCalls: [ query_order({\customerName\:\张三\,\date\:\2024-05-21\}), send_email({\toEmail\:\zhangsanexample.com\,\orderData\:\{\\\orderNo\\\:\\\ORD20240521001\\\,\\\status\\\:\\\shipped\\\,\\\amount\\\:299.00}\}) ] }注意LLM把“今天”自动转成了2024-05-21这是Qwen内置的日期推理能力不用你写代码处理。第二阶段工具执行与结果query_order执行SQLSELECT ... WHERE customer_name 张三 AND DATE(create_time) 2024-05-21返回空列表因为数据库里只有5月20日的订单。send_email因orderData为空抛出ToolExecutionException被onError捕获返回“订单系统暂时不可用”。第三阶段LLM整合错误并生成回复最终返回{ response: 抱歉没找到张三今天下的订单。请确认日期是否正确或稍后再试。, toolCalls: [query_order(...), send_email(...)] }这个过程里你能在IDEA的Debug模式下清楚看到firstResponse.toolExecutionRequests()里有两个ToolExecutionRequest对象results列表里有两个ToolExecutionResult其中第二个是错误结果。这种透明性是调试复杂工具链的黄金标准。5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 典型问题速查表问题现象根本原因排查步骤解决方案LLM始终不调用任何工具只返回自由文本ToolSpecification.description太笼统或LLM没被明确告知“可用工具”1. 检查AiServices是否传入了toolExecutor()2. 在description末尾加一句“请优先使用以下工具”重写description加入动词引导“请使用query_order工具查询订单再用send_email工具发送通知”工具调用后返回nullLLM说“没找到结果”ToolResult.of(null)被序列化为空字符串LLM无法解析1. 在工具方法里加log.info(result{}, result)2. 检查ToolResult.of()传入的对象是否为null永远不要传null用ToolResult.of(Collections.emptyList())或ToolResult.of(无数据)邮件里中文乱码显示为?Freemarker模板编码与邮件MIME头不一致1. 查看邮件原始源码确认Content-Type: text/html; charsetISO-8859-12. 检查application.yml里spring.mail.default-encoding在application.yml加spring.mail.default-encoding: UTF-8并在MimeMessageHelper.setText()时指定UTF-8多次请求后LLM开始“幻觉”工具名比如把query_order说成get_orderChatMemory缓存了错误的历史污染了上下文1. 清空InMemoryChatMemory2. 检查maxMessages是否设得太小将maxMessages设为20以上或改用RedisChatMemory做持久化隔离本地Qwen响应极慢CPU占满但无输出llama.cpp的-c上下文长度设得过大超出内存1.top命令看进程RSS内存2.llama.cpp启动日志是否有failed to allocate将-c 2048改为-c 1024或升级到最新版llama.cpp5.2 参数提取失败的三大元凶参数提取是Tool Calling最脆弱的环节。我统计过23个失败案例87%源于以下三类时间格式歧义用户说“上周五”LLM可能输出2024-05-17正确或2024-05-10错误。解决方案不是让LLM猜而是在QueryOrderTool.execute()里加日期归一化逻辑if (dateStr.contains(上周)) { LocalDate now LocalDate.now(); date now.with(DayOfWeek.MONDAY).minusWeeks(1); // 强制取上周一 }客户姓名模糊匹配数据库存的是“张三丰”用户说“张三”直接查询失败。在SQL里改用LIKEString sql SELECT ... WHERE customer_name LIKE ?; jdbcTemplate.queryForList(sql, % customerName %);参数名大小写不一致LLM输出{CustomerName:张三}但ParameterSpec.name是customerName。LangChain4j默认不忽略大小写。解决方案是在ToolExecutor里加转换.onBeforeExecute((request, tool) - { MapString, Object normalized new HashMap(); request.parameters().forEach((k, v) - normalized.put(k.toLowerCase(), v)); // 统一小写 return new ToolExecutionRequest(request.name(), normalized); })5.3 性能优化实战从30秒到3秒的响应提速默认配置下一次“查发”耗时约25秒Qwen推理15秒 JDBC查询3秒 邮件发送7秒。优化后压到3秒内LLM侧Qwen2-7B用AWQ量化后推理速度提升2.3倍关闭logprobs概率输出减少30%计算量temperature0.3比0.7快1.8倍。数据库侧在orders表的customer_name和create_time字段上建联合索引查询从1.2秒降到0.03秒。邮件侧用JavaMailSender的setSession()复用SMTP连接避免每次新建TCP握手邮件模板预编译freemarkerConfig.setTemplateUpdateDelay(30000)避免每次渲染都解析FTL。框架侧LangChain4j的ToolExecutor默认串行执行但query_order和send_email无依赖可并行.executor(Executors.newFixedThreadPool(2)) // 改为线程池最终压测结果并发100请求P95响应时间2.8秒错误率0%。这已经满足内部运营系统SLA5秒。5.4 安全红线必须守住的三条底线绝不让LLM拼接SQL所有数据库操作必须封装在工具方法里用JdbcTemplate的?占位符。我见过有人写String sql SELECT * FROM orders WHERE customer_name customerName ;这是SQL注入的温床。邮箱地址必须白名单校验SendEmailTool里加正则if (!toEmail.matches(^[A-Za-z0-9_.-][A-Za-z0-9.-]\\.[A-Za-z]{2,}$)) { throw new ToolExecutionException(邮箱格式错误); }并在生产环境配置spring.mail.host
阅读完成 · 觉得有帮助?