LangChain4j的Prompt 注解体系
初步体验了AiService的声明式魅力——定义接口框架生成实现。但很多同学在实际使用中会遇到几个问题System Prompt 能不能带变量比如不同租户有不同的公司名称、服务范围。复杂的 Prompt 能不能放在外部文件里让非开发人员也能修改如何在运行时动态决定 System PromptSaaS 多租户场景这一节我们将把AiService相关的注解彻底讲透包括SystemMessage、UserMessage、V以及从文件加载 Prompt 等多个技巧。一、SystemMessage定义角色的根基SystemMessage对应我们熟悉的 System Prompt用于设定模型的角色、行为规范和输出格式。1.1 基础用法AiService public interface Assistant { SystemMessage(你是一个 Java 技术助手用简洁的语言回答代码用 Java 17 语法) String chat(String question); }1.2 多行 System PromptText Block对于复杂的 Prompt使用 Java 15 引入的文本块会让代码更清爽AiService public interface CustomerServiceAssistant { SystemMessage( 你是鸡翅商城的智能客服助手小鸡。 服务范围商品咨询、订单查询、售后服务 约束 - 只回答与鸡翅商城相关的问题 - 不确定的信息说我帮您查一下不要猜测 - 回复不超过 150 字 - 涉及投诉主动提出转人工 ) String chat(String userMessage); }实际使用时直接注入接口即可RestController public class CustomerController { private final CustomerServiceAssistant assistant; // 构造器注入 GetMapping(/cs) public String ask(RequestParam String msg) { return assistant.chat(msg); } }1.3 System Prompt 中的变量注入V如果你的 System Prompt 需要根据不同调用动态变化比如多租户场景可以在模板中使用{{variableName}}占位符并在方法参数上用V标记变量值。AiService public interface TenantAssistant { SystemMessage(你是{{companyName}}的客服助手服务范围{{serviceScope}}) String chat(V(companyName) String company, V(serviceScope) String scope, UserMessage String userMessage); // UserMessage 标记用户输入 }调用时curl http://localhost:8080/tenant/chat?company鸡翅商城scope商品、订单message我的订单在哪注意UserMessage用于指明哪个参数是用户的实时输入后面会详细解释。二、UserMessage精细控制用户消息的格式默认情况下AiService方法中唯一的String参数会被当作 User Message。但有时我们希望对用户消息做更复杂的包装比如加上“请翻译成英文”的前缀或者动态拼接内容。2.1 在注解中定义消息模板AiService public interface Translator { SystemMessage(你是一个专业翻译) UserMessage(将以下文字翻译成{{language}}\n{{text}}) String translate(V(language) String lang, V(text) String text); }这样就不需要在每个调用点手动拼接字符串了。2.2 标记参数即 User Message如果不使用UserMessage模板也可以直接在参数上标记UserMessage表明该参数的内容就是用户消息AiService public interface CodeAssistant { SystemMessage(你是一个 Java 代码审查专家) String reviewCode(UserMessage String code); }三、V——变量注入的多种方式V用于向SystemMessage或UserMessage中的占位符提供值。除了基本类型和字符串它还能自动展开 Java BeanRecord 或 POJO的字段。3.1 多个独立变量AiService public interface ReportGenerator { SystemMessage(你是一个数据分析专家) UserMessage( 根据以下数据生成一份{{reportType}}报告 时间范围{{startDate}} 至 {{endDate}} 数据{{data}} ) String generateReport( V(reportType) String type, V(startDate) String start, V(endDate) String end, V(data) String data ); }3.2 对象参数自动展开如果参数是一个 Record 或普通 Java BeanLangChain4j 会自动将其字段名作为变量名进行匹配。package com.jichi.langchain4j.model; public record CodeReviewRequest( String language, String code, String focusAreas // 性能、空指针、事务 ) {}package com.jichi.langchain4j.service; import com.jichi.langchain4j.model.CodeReviewRequest; import dev.langchain4j.service.SystemMessage; import dev.langchain4j.service.UserMessage; import dev.langchain4j.service.spring.AiService; AiService public interface CodeReviewer { SystemMessage(你是代码审查专家) UserMessage( 请 review 以下 {{language}} 代码 {{language}} {{code}} 重点关注{{focusAreas}} ) String review(CodeReviewRequest request); // ↑ Record 的字段名就是模板变量名 }四、从文件加载 Prompt分离配置与代码将 Prompt 写在注解中每次修改都需要重新编译、打包、部署。对于需要频繁调整或非开发人员维护的场景最合适的做法是把 Prompt 放到外部资源文件中。4.1 目录结构src/main/resources/ └── prompts/ ├── customer-service.txt ├── code-review.txt └── report-generator.txt4.2 文件内容示例customer-service.txt你是{{companyName}}的智能客服助手。 服务范围{{serviceScope}} 约束 - 只回答与{{companyName}}相关的问题 - 不确定的说我帮您确认一下不要猜测 - 回复不超过 150 字4.3 在注解中引用文件使用fromResource属性指定文件路径相对于resources目录AiService public interface FileBasedAssistant { SystemMessage(fromResource prompts/customer-service.txt) String chat(V(companyName) String company, V(serviceScope) String scope, UserMessage String message); }这样当产品经理需要修改话术时只需要替换文件内容应用无需重启配合热加载机制即可生效。五、注解优先级与小贴士当同一个类或接口中多个方法都有SystemMessage时每个方法独立生效互不影响。如果某个方法没有标注SystemMessage则不会发送 System 消息。UserMessage模板的优先级高于默认的参数即消息的规则。从文件加载模板时文件内容仍然可以包含{{variable}}占位符并且可以和V配合使用。方法上的 SystemMessage 优先级高于 类上的 SystemMessage六、完整示例多场景助手package com.jichi.langchain4j.service; import dev.langchain4j.service.SystemMessage; import dev.langchain4j.service.UserMessage; import dev.langchain4j.service.V; import dev.langchain4j.service.spring.AiService; AiService public interface MultiScenarioAssistant { // 场景一简单问答 SystemMessage(你是一个 Java 技术助手回答简洁) String techChat(String question); // 场景二有格式要求的翻译 SystemMessage(你是专业翻译保持原文风格不添加解释) UserMessage(将以下内容翻译成{{language}}\n\n{{content}}) String translate(V(language) String lang, V(content) String content); // 场景三从文件加载复杂 Prompt SystemMessage(fromResource prompts/code-review.txt) String reviewCode(V(language) String lang, UserMessage String code); // 场景四多变量 对象参数 SystemMessage(你是数据分析专家) UserMessage(分析以下{{period}}的销售数据重点关注{{focus}}\n{{data}}) String analyzeData(V(period) String period, V(focus) String focus, V(data) String data); }package com.jichi.langchain4j.controller; import com.jichi.langchain4j.service.MultiScenarioAssistant; import org.springframework.web.bind.annotation.*; RestController RequestMapping(/prompt/multi) public class MultiScenarioController { private final MultiScenarioAssistant assistant; public MultiScenarioController(MultiScenarioAssistant assistant) { this.assistant assistant; } GetMapping(/tech) public String techChat(RequestParam String question) { return assistant.techChat(question); } GetMapping(/translate) public String translate(RequestParam String language, RequestParam String content) { return assistant.translate(language, content); } PostMapping(/review) public String reviewCode(RequestParam String language, RequestBody String code) { return assistant.reviewCode(language, code); } GetMapping(/analyze) public String analyzeData(RequestParam String period, RequestParam String focus, RequestParam String data) { return assistant.analyzeData(period, focus, data); } }# 场景一技术问答 curl http://localhost:8080/prompt/multi/tech?question什么是CompletableFuture # 场景二翻译 curl http://localhost:8080/prompt/multi/translate?language英文content人工智能正在改变软件开发方式 # 场景三代码 review需要 prompts/code-review.txt 文件存在 curl -X POST http://localhost:8080/prompt/multi/review?languageJava \ -H Content-Type: text/plain \ -d public ListUser getAll() { return userRepo.findAll(); } # 场景四数据分析 curl http://localhost:8080/prompt/multi/analyze?period2025Q1focus增长趋势data销售额1200万同比15%