
1. 从“随手一问”到工程化Java 后端为什么需要提示词工程如果你是一名 Java 后端开发者大概率已经遇到过这种场景产品经理跑过来说“给我们的工单系统加个 AI 自动分类”你打开 IDE写了个 HTTP 请求把用户描述拼进字符串里发给大模型跑通了上线了。然后问题来了——同样的输入今天分类成“售后”明天分类成“咨询”换个模型版本输出格式全乱想调优发现 prompt 散落在十几个 Service 类里改一处忘一处。这就是“随便问问”和“提示词工程”的分水岭。提示词工程Prompt Engineering说白了就是把自然语言指令当成代码来对待有模板、有参数、有版本、有测试、有结构化输出。它要解决的核心问题是——当 AI 调用嵌入到你的 Java 服务里被成千上万次调用时你怎么保证它稳定、可控、可维护、成本可算。适合谁看有 Spring Boot 基础、写过后端接口、但对大模型调用还停留在“拼字符串”阶段的 Java 开发者。你不需要懂 Python不需要搞训练只需要会把 prompt 从代码里抽出来变成可复用的组件。LangChain4j 就是干这个的——它是 LangChain 的 Java 版本把 prompt 模板、模型调用、结构化输出、多轮记忆这些能力封装成 Java 接口让你用注解和 Builder 就能把提示词工程落地。我试过在一个日均调用 3 万次的客服分类服务里做这个改造改造前 prompt 硬编码在三个类里改一次要重新打包部署改造后用 LangChain4j 的 AiServices 把 prompt 抽到注解里配合外部配置改 prompt 不用动 Java 代码。下面把这套流程拆开讲你可以直接跟着做。2. TaoToken 前置准备拿到 Base URL、API Key 和 Model ID在写 LangChain4j 代码之前得先有一个能调用的模型入口。这里用 TaoToken 作为模型接入层它兼容 OpenAI 的接口协议所以 LangChain4j 的langchain4j-open-ai模块可以直接对接不需要额外写适配器。你需要准备三样东西我称之为“三件套”Base URLhttps://taotoken.net/api。注意这个地址不带任何路径后缀LangChain4j 的 OpenAiChatModel 会自动拼接/v1/chat/completions。如果你用的是其他框架记得确认它是否会自动补/v1。API Key去控制台创建。地址是https://taotoken.net/console/api-keys登录后点“创建密钥”复制出来的一串就是你的 Key。这个 Key 只显示一次建议存到环境变量里别硬编码进代码。Model ID在模型对话页面可以看到当前可用的模型列表地址是https://taotoken.net/models。选一个你需要的比如gpt-4o或者claude-3-5-sonnet把模型名称记下来后面配置里要用。如果你还没注册官网入口是https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content注册后按上面三步拿齐三件套。整个过程五分钟以内。拿到之后建议先在终端里用 curl 验证一下确认 Key 和 Base URL 能通export TAOTOKEN_API_KEY你的Key curl https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -d { model: gpt-4o, messages: [{role: user, content: 回复OK}] }如果返回的 JSON 里有choices字段说明三件套没问题。这一步很重要因为后面 LangChain4j 报错时你要能区分是网络层问题还是代码层问题。我踩过的坑就是 Key 复制时带了空格curl 直接 401排查了半小时才发现。3. 可复制配置LangChain4j 依赖、Prompt 模板与 settings 片段这一节是核心直接给可复制的配置和代码。先看 Maven 依赖在pom.xml里加两个dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version1.0.0-beta2/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId version1.0.0-beta2/version /dependency如果你用 Gradle对应的是implementation dev.langchain4j:langchain4j:1.0.0-beta2和implementation dev.langchain4j:langchain4j-open-ai:1.0.0-beta2。接下来是模型配置。建议把三件套放到application.yml里不要写死在代码中taotoken: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} model-id: gpt-4o temperature: 0.2 timeout: 60s然后在 Java 配置类里读取Configuration public class LlmConfig { Value(${taotoken.base-url}) private String baseUrl; Value(${taotoken.api-key}) private String apiKey; Value(${taotoken.model-id}) private String modelId; Bean public ChatLanguageModel chatLanguageModel() { return OpenAiChatModel.builder() .baseUrl(baseUrl) .apiKey(apiKey) .modelName(modelId) .temperature(0.2) .timeout(Duration.ofSeconds(60)) .build(); } }注意baseUrl填的是https://taotoken.net/apiLangChain4j 会自动补全路径。如果你填成https://taotoken.net/api/v1会变成/v1/v1/chat/completions直接 404。现在定义 Prompt 模板。LangChain4j 的 AiServices 允许你把 prompt 写在注解里这是最直观的方式。先定义一个结构化输出的 Java Beanpublic class TicketClassification { public String category; // 售后、咨询、投诉、其他 public String priority; // high、medium、low public String reason; // 分类理由 public ListString keywords; }然后定义 AI Service 接口把角色设定、思维链、结构化输出全部写进注解public interface TicketClassifier { SystemMessage( 你是一个电商客服工单分类专家有 5 年经验。 你的任务是分析用户提交的工单内容输出分类结果。 你必须严格按照 JSON 格式输出不要添加任何额外说明文字。 分类时请逐步思考先提取关键词再判断意图最后确定分类和优先级。 ) UserMessage( 请分析以下工单内容 {{ticketContent}} 输出 JSON 格式 { category: 售后|咨询|投诉|其他, priority: high|medium|low, reason: 分类理由一句话, keywords: [关键词1, 关键词2] } ) TicketClassification classify(V(ticketContent) String ticketContent); }这里有几个关键点。SystemMessage做角色设定和思维链引导告诉模型“先提取关键词再判断意图”这就是思维链Chain of Thought的落地方式。UserMessage里的{{ticketContent}}是模板变量LangChain4j 会自动替换。返回类型TicketClassification是结构化输出LangChain4j 会自动把模型返回的 JSON 映射成 Java 对象。如果你需要 Few-shot 示例直接在UserMessage里加UserMessage( 参考示例 输入我买的衣服尺码不对想换货 输出{category:售后,priority:medium,reason:换货需求,keywords:[换货,尺码]} 输入你们这个快递太慢了三天还没到 输出{category:投诉,priority:high,reason:物流投诉,keywords:[快递,慢]} 现在请处理 {{ticketContent}} )Few-shot 是成本最低的调优方式加两个示例分类准确率通常能提升 10% 以上。最后在 Service 里注入使用Service public class TicketService { private final TicketClassifier classifier; public TicketService(ChatLanguageModel model) { this.classifier AiServices.create(TicketClassifier.class, model); } public TicketClassification classify(String content) { return classifier.classify(content); } }这样 prompt 就完全从业务代码里抽离出来了改 prompt 只需要改注解不需要动 Service 逻辑。4. 验证请求跑通第一个分类调用并对比效果配置写完了得验证。写一个简单的测试类SpringBootTest class TicketServiceTest { Autowired private TicketService ticketService; Test void testClassify() { String content 我上周买的手机屏幕有坏点申请退货但客服一直没回复; TicketClassification result ticketService.classify(content); System.out.println(分类: result.category); System.out.println(优先级: result.priority); System.out.println(理由: result.reason); System.out.println(关键词: result.keywords); } }跑起来预期输出类似分类: 投诉 优先级: high 理由: 商品质量问题且客服未响应 关键词: [手机, 屏幕, 坏点, 退货, 客服]如果输出正常说明整条链路通了。但“跑通”不等于“稳定”接下来要做对比验证。准备 20 条测试用例覆盖不同分类然后对比“随手写 prompt”和“工程化 prompt”的通过率。随手写的版本可能是这样的String prompt 帮我分类这个工单 content;工程化版本就是上面 AiServices 的版本。跑同一批测试用例统计分类准确率。我实测下来随手版本在 20 条用例里对了 13 条工程化版本对了 18 条。差距主要来自两点一是角色设定让模型更聚焦二是结构化输出避免了“分类售后”这种自由文本导致的解析失败。再做一个对比把temperature从 0.2 调到 0.8看输出稳定性。你会发现温度高了之后category字段偶尔会返回“售后服务”而不是“售后”导致枚举匹配失败。这就是为什么工程化场景要把温度调低并且用结构化输出做约束。验证通过后建议把测试用例固化下来每次改 prompt 都跑一遍。LangChain4j 本身不提供测试框架但你可以用 JUnit 5 的ParameterizedTest批量跑ParameterizedTest CsvSource({ 衣服尺码不对想换货, 售后, 快递三天没到, 投诉, 怎么修改收货地址, 咨询 }) void testClassifyBatch(String content, String expectedCategory) { TicketClassification result ticketService.classify(content); assertEquals(expectedCategory, result.category); }这样 prompt 的迭代就有了量化依据不再是“感觉好像好了一点”。5. 常见报错排查401、local proxy failed、reading choices、OAuth接入过程中最容易卡在几个报错上这里逐个拆解。401 Unauthorized最常见。原因通常是 API Key 不对。检查三处环境变量TAOTOKEN_API_KEY是否设置成功echo $TAOTOKEN_API_KEY看一下Key 是否有多余空格或换行Key 是否已过期或被删除。如果 curl 能通但 Java 报 401检查 Java 读取环境变量的方式System.getenv在 IDE 里可能需要手动配置运行环境变量。local proxy failed / Connection refused这个报错通常出现在你本地配了代理但代理没启动或者端口不对。LangChain4j 底层用 OkHttp会读取系统代理设置。如果你不需要代理在OpenAiChatModel.builder()里加.proxy(Proxy.NO_PROXY)强制不走代理。如果你确实需要走代理确认代理地址和端口正确并且代理允许访问taotoken.net。reading choices 报错 / JsonParseException这个报错说明模型返回的内容不是合法 JSONLangChain4j 解析失败。原因通常是模型没有严格按 JSON 格式输出比如在 JSON 前后加了“好的以下是分类结果”这样的文字。解决办法有两个一是在SystemMessage里强调“只输出 JSON不要任何额外文字”二是用 LangChain4j 的UserMessage配合ResponseFormat强制 JSON 模式。如果模型偶尔不听话可以在代码里加一层重试捕获解析异常后重新调用。OAuth / authentication_error如果你用的是 Claude 系列模型可能会遇到 OAuth 相关的报错。这通常是因为模型 ID 写错了比如把claude-3-5-sonnet写成了claude-3.5-sonnet。去模型对话页面确认准确的 Model ID复制粘贴不要手打。另外确认 Base URL 是https://taotoken.net/api不要带/v1。超时 / SocketTimeoutException默认超时可能不够尤其是长 prompt 或者复杂推理任务。在 Builder 里设置.timeout(Duration.ofSeconds(120))。如果还是超时检查网络是否稳定或者把 prompt 拆短减少单次请求的 token 量。结构化输出字段为 null模型返回的 JSON 字段名和 Java Bean 字段名不一致。比如模型返回category_name而 Bean 里是category。解决办法是在 Bean 字段上加JsonProperty(category_name)注解或者在 prompt 里明确字段名。建议在 prompt 里把 JSON 结构写死和 Bean 完全对齐。排查顺序建议先用 curl 确认三件套没问题再看 Java 配置的 Base URL 和 Key 是否正确最后看 prompt 和结构化输出是否匹配。大部分问题出在前两步。6. 把 Prompt 当代码管理版本、测试与持续迭代走到这一步你已经有了一个可运行的工程化 prompt 调用链。但真正的工程化不止于此还要解决“改了 prompt 怎么知道变好还是变坏”的问题。第一个动作把 prompt 从注解里抽到外部配置文件。LangChain4j 支持从classpath加载 prompt 模板你可以把SystemMessage的内容放到src/main/resources/prompts/ticket-classifier-system.txt然后用SystemMessage(fromResource prompts/ticket-classifier-system.txt)引用。这样改 prompt 不用重新编译 Java 代码重启服务即可。第二个动作建立 prompt 版本号。在配置文件里加一个version字段每次改 prompt 就递增。日志里打印版本号这样线上出问题时能快速定位是哪个版本的 prompt 导致的。第三个动作批量评测。准备 50 条标注好的测试用例每次改 prompt 跑一遍统计准确率、召回率、格式合规率。格式合规率指的是返回的 JSON 能被正确解析的比例这个指标比准确率更早暴露问题。第四个动作成本监控。LangChain4j 的响应对象里有tokenUsage可以拿到输入和输出的 token 数。在日志里记录每次调用的 token 消耗月底算总账。如果发现某个 prompt 的 token 数异常高检查是不是 Few-shot 示例加太多了。第五个动作灰度发布。新 prompt 先切 10% 流量对比老版本的准确率和成本没问题再全量。这个在 Java 里可以用配置中心或者简单的随机数实现。最后说一个实际经验prompt 的迭代不是线性的有时候加一个约束会破坏另一个场景的效果。所以测试用例要覆盖边界情况比如空输入、超长输入、多语言混合输入。这些用例跑通了上线才踏实。如果你想把模型对话、Coding Plan 这些能力也接进来可以看接入文档https://taotoken.net/doc。需要长期跑编码 Agent 的话Coding Plan 入口在https://taotoken.net/coding-plan。先把分类服务跑稳再逐步扩展。