【LangChain4J-06】Prompt 工程与模板化开发
- [✍️ LangChain4J(6) Prompt 工程与模板化开发](#✍️ LangChain4J(6) Prompt 工程与模板化开发)
- [🧭 一、Prompt 工程核心思想](#🧭 一、Prompt 工程核心思想)
-
- [1.1 一句话](#1.1 一句话)
- [1.2 三个核心原则](#1.2 三个核心原则)
- [1.3 结构化 Prompt 写法(本项目的落地形式)](#1.3 结构化 Prompt 写法(本项目的落地形式))
- [1.4 关键术语速查(全文高频词,先看懂再往下读)](#1.4 关键术语速查(全文高频词,先看懂再往下读))
- [🧬 二、LangChain4J PromptTemplate 模板引擎](#🧬 二、LangChain4J PromptTemplate 模板引擎)
-
- [2.1 什么是 PromptTemplate](#2.1 什么是 PromptTemplate)
- [2.2 为什么用它而不是字符串拼接](#2.2 为什么用它而不是字符串拼接)
- [2.3 本项目用法(统一封装)](#2.3 本项目用法(统一封装))
- [🔖 三、变量占位符、动态 Prompt 生成](#🔖 三、变量占位符、动态 Prompt 生成)
-
- [3.1 占位符语法](#3.1 占位符语法)
- [3.2 动态生成示例(L9 代码生成器的业务层)](#3.2 动态生成示例(L9 代码生成器的业务层))
- [3.3 实现思路(变量从哪来)](#3.3 实现思路(变量从哪来))
- [🎯 四、Few-shot 示例学习(给模型样例提升准确率)](#🎯 四、Few-shot 示例学习(给模型样例提升准确率))
-
- [4.1 为什么 Few-shot 有效](#4.1 为什么 Few-shot 有效)
- [4.2 本项目的 Few-shot 效果(实测对比)](#4.2 本项目的 Few-shot 效果(实测对比))
- [4.3 样例库(统一管理)](#4.3 样例库(统一管理))
- [🧱 五、Prompt 分层设计(人设 + 规则 + 业务 + 样例)](#🧱 五、Prompt 分层设计(人设 + 规则 + 业务 + 样例))
-
- [5.1 四层定义(`PromptLayer` 枚举)](#5.1 四层定义(
PromptLayer枚举)) - [5.2 为什么这样分](#5.2 为什么这样分)
- [5.3 拼装实现(`PromptTemplateBuilder`)](#5.3 拼装实现(
PromptTemplateBuilder))
- [5.1 四层定义(`PromptLayer` 枚举)](#5.1 四层定义(
- [💾 六、Prompt 缓存、复用、统一管理](#💾 六、Prompt 缓存、复用、统一管理)
-
- [6.1 统一管理:模板集中定义](#6.1 统一管理:模板集中定义)
- [6.2 缓存:编译一次、多次使用](#6.2 缓存:编译一次、多次使用)
- [6.3 复用:同一模板、不同变量](#6.3 复用:同一模板、不同变量)
- [⚙️ 七、L9 / L10 实现原理(说清楚"它是怎么工作的")](#⚙️ 七、L9 / L10 实现原理(说清楚"它是怎么工作的"))
-
- [7.1 L9 · Prompt 代码生成器(`prompt-code`)的实现原理](#7.1 L9 · Prompt 代码生成器(
prompt-code)的实现原理) - [7.2 L10 · Prompt 文本分类器(`prompt-classify`)的实现原理](#7.2 L10 · Prompt 文本分类器(
prompt-classify)的实现原理) - [7.3 两者的共同架构(为什么说"同一套代码")](#7.3 两者的共同架构(为什么说"同一套代码"))
- [7.1 L9 · Prompt 代码生成器(`prompt-code`)的实现原理](#7.1 L9 · Prompt 代码生成器(
- [🛣️ 八、L9 / L10 实现过程(从零到可用的 8 步)](#🛣️ 八、L9 / L10 实现过程(从零到可用的 8 步))
-
- [8.1 落地步骤清单](#8.1 落地步骤清单)
- [8.2 一次请求的服务端日志(完整"实现过程"实拍)](#8.2 一次请求的服务端日志(完整"实现过程"实拍))
- [8.3 实现前后对比(代码量视角)](#8.3 实现前后对比(代码量视角))
- [🏢 九、L9 / L10 使用场景与真实企业案例](#🏢 九、L9 / L10 使用场景与真实企业案例)
-
- [9.1 L9(代码生成器)适用场景](#9.1 L9(代码生成器)适用场景)
- [9.2 L10(文本分类器)适用场景](#9.2 L10(文本分类器)适用场景)
- [9.3 企业真实案例(学习参考,非本项目部署)](#9.3 企业真实案例(学习参考,非本项目部署))
- [9.4 案例共性总结](#9.4 案例共性总结)
- [⚖️ 十、没有 L9 / L10 会怎样?(实现前后对比)](#⚖️ 十、没有 L9 / L10 会怎样?(实现前后对比))
-
- [10.1 总体对比](#10.1 总体对比)
- [10.2 具体场景前后对比](#10.2 具体场景前后对比)
- [📚 十一、项目文件速查](#📚 十一、项目文件速查)
✍️ LangChain4J(6) Prompt 工程与模板化开发
📗 本文档记录 Spring Boot 3 + JDK 17 + LangChain4j + Ollama(qwen3:4b-instruct) 下「Prompt 工程 + 模板化开发」的完整学习笔记与落地实现。
项目已落地 L9 · Prompt 代码生成器 与 L10 · Prompt 文本分类器 两个新模式(对话模式下拉自动出现),作为六个知识点的实战案例。
💡 阅读提示:紫色 = 枚举 / 注解,蓝色 = 类 / API,橙色 = 模板 / 占位符,红色 = 注意点,绿色 = 正向结论 / 效果。
🧭 一、Prompt 工程核心思想
1.1 一句话
Prompt 工程 = 用「结构清晰、约束明确、示例充分」的指令,让大模型稳定地输出你想要的答案。
大模型不是搜索引擎,它是对着输入做续写。你给的指令越清晰,它"猜"对意图的概率越高。
1.2 三个核心原则
| 原则 | 说明 | 反例 → 正例 |
|---|---|---|
| 🎯 意图明确 | 告诉模型「要什么」而不是「别给什么」 | "别写太差" → "输出规范可编译的 Java 代码" |
| 📐 结构清晰 | 用分段/编号/格式模板组织指令,模型更容易遵循 | 一段长文本 → 人设/规则/业务/样例四层分块 |
| 🧪 示例充分 | Few-shot 让模型"照着做",比规则更直观 | 只说"输出 JSON" → 给 3 组 JSON 示例 |
1.3 结构化 Prompt 写法(本项目的落地形式)
本项目所有 Prompt 都按「四层」组织(详见第五章),每层一个明确职责:
【人设层】你是一名资深 Java/Spring Boot 架构师...
【规则层】1. 只输出代码... 2. 必须含异常处理... 3. ...
【样例层】示例 1: 需求→代码...
【业务层】需求:{{requirement}};语言:{{language}}
1.4 关键术语速查(全文高频词,先看懂再往下读)
| 术语 | 详细描述 |
|---|---|
| Prompt(提示词) | 发给大模型的全部输入文本。大模型是「续写机器」------你给它什么前缀,它就按概率续写出最像样的后缀。Prompt 的质量直接决定输出的质量,这就是"Prompt 工程"的由来。 |
| 续写机制 | 大模型(如 qwen3:4b)本质上在做逐 token 概率预测:根据已有文本预测下一个 token(≈词/子词)最可能是哪个。指令清晰 → 高概率路径明确 → 输出稳定;指令模糊 → 多条路径概率接近 → 输出漂移。 |
| Token | 大模型处理文本的最小单元,中文通常 1 个汉字 ≈ 1~2 个 token。计费、上下文窗口都以 token 计。 |
| 上下文窗口(Context Window) | 模型一次能"看到"的 token 上限(qwen3:4b 通常 8K~32K)。系统层 + 历史消息 + 本轮输入合计不能超窗,超了会被截断或报错------这也是分层设计中"固化层不能无限膨胀"的原因。 |
| SystemMessage(系统消息) | 设定全局角色/规则的"最高优先级指令",一般放消息列表第一条,模型优先遵循。本项目的人设层/规则层/样例层都拼进 SystemMessage。 |
| UserMessage(用户消息) | 用户本轮输入(业务层动态内容)。 |
| AiMessage(模型消息) | 模型上一轮的回答,写回记忆后作为历史参与后续轮次。 |
| Temperature(温度) | 采样随机性参数,0~1+。越低越确定(分类、代码适合 0.1~0.4),越高越发散/有创意(文案、头脑风暴适合 0.7+)。 |
| Few-shot(少样本学习) | 在 Prompt 里给若干组「输入→理想输出」示例,让模型模仿格式与风格。零样本(zero-shot) = 只给指令不给示例。对小模型,Few-shot 通常远胜 zero-shot。 |
| 结构化输出(JSON) | 用规则层 + 样例层强制模型只输出合法 JSON,方便程序直接解析,避免"自然语言 + 格式漂移"。 |
| 置信度(confidence) | 模型对自己分类判断的把握(0~1)。本项目 L10 输出它,供下游做阈值判断(如 <0.7 转人工)。 |
| 占位符({{var}}) | 模板里的变量插槽,渲染时用 Map 替换,实现"一个模板、多变指令"。 |
| 多轮记忆(sessionId) | 以 sessionId 为 key 的会话记忆,把历史对话拼进每次请求,让模型"记得"上文。 |
🧬 二、LangChain4J PromptTemplate 模板引擎
2.1 什么是 PromptTemplate
LangChain4j 内置的模板引擎(dev.langchain4j.model.input.PromptTemplate):把「固定的提示词骨架」与「动态变量」分离。
java
import dev.langchain4j.model.input.PromptTemplate;
// 模板:用 {{变量}} 占位
String templateText = "你好,我叫 {{name}},是一名 {{role}}。";
// 编译模板 → 填充变量 → 得到 Prompt
PromptTemplate template = PromptTemplate.from(templateText);
Prompt prompt = template.apply(Map.of("name", "小明", "role", "Java 工程师"));
String text = prompt.text(); // "你好,我叫 小明,是一名 Java 工程师。"
技术细节:
PromptTemplate.from(text)做一次模板编译 (解析{``{...}}占位符,内部转成插槽模型),编译结果可反复apply;apply(Map)是纯函数 :每次调用返回新的Prompt对象,不修改模板本身,天然支持并发;- 占位符支持重复出现 (
{``{name}}出现 3 次会全部替换);模板未提到的变量会被忽略;模板有但 Map 缺失的变量 →apply抛异常,把"漏变量"的 bug 提前暴露在开发期。
2.2 为什么用它而不是字符串拼接
| 字符串拼接 | PromptTemplate |
|---|---|
| 模板散落在代码各处 | 模板与渲染分离,可集中管理 |
| 变量缺失靠运气 | apply() 对缺失变量抛异常,早发现早修复 |
| 每次拼接重复劳动 | 编译一次、缓存复用(见第六章) |
| 无迹可查 | 模板即文档,结构一目了然 |
| 拼接容易忘换行/引号转义 | 文本块(Java 15+ text block """)原样书写 |
2.3 本项目用法(统一封装)
java
// chat/prompt/PromptTemplateStore.java
public String render(String templateText, Map<String, Object> variables) {
// computeIfAbsent:首次编译并缓存,之后命中缓存(缓存/复用)
PromptTemplate template = cache.computeIfAbsent(templateText, PromptTemplate::from);
return template.apply(variables).text();
}
🔖 三、变量占位符、动态 Prompt 生成
3.1 占位符语法
- 模板中用
{``{变量名}}声明占位符; - 渲染时用
Map.of("变量名", 值)替换; - 模板里未出现的变量会被忽略;模板里有但 Map 没给 → 抛异常。
java
String tpl = "把「{{text}}」翻译成{{target}}";
PromptTemplate.from(tpl).apply(Map.of("text", "你好", "target", "英语")).text();
// → "把「你好」翻译成英语"
3.2 动态生成示例(L9 代码生成器的业务层)
java
// chat/prompt/PromptTemplates.java ------ 业务层模板(含占位符)
public static final String CODE_GEN_BUSINESS = """
需求:{{requirement}}
语言:{{language}}
框架:{{framework}}""";
// PromptCodeChatHandler ------ 每次请求用不同变量渲染
String userPrompt = promptBuilder.buildBusiness(PromptTemplates.CODE_GEN_BUSINESS,
Map.of("requirement", req.getMessage(), "language", "Java", "framework", "Spring Boot"));
同一个模板,换变量就是一条新指令 ------ 这就是动态 Prompt 生成 + 模板复用。
3.3 实现思路(变量从哪来)
| 变量 | 来源 | 例子 |
|---|---|---|
| 用户输入 | req.getMessage() |
"写一个定时任务清理过期订单" |
| 业务上下文 | 后端注入(当前固定) | language=Java、framework=Spring Boot |
| 运行时数据 | 后续可扩展(如数据库配置) | 从配置表读取企业自己的代码规范 |
💡 扩展方向:把
language/framework从"硬编码固定值"改成"前端下拉参数",就是 5.3 节"业务层可替换"的活例子------模板一行不用改,只换变量来源。
🎯 四、Few-shot 示例学习(给模型样例提升准确率)
4.1 为什么 Few-shot 有效
对 4B 小模型(如 qwen3:4b),"听规则"不如"看样例":模型通过模仿示例的输出格式与风格,比死记规则稳定得多。规则说"输出 JSON",模型可能加解释;给出 3 组 JSON 示例后,它"照葫芦画瓢"直接输出 JSON。
zero-shot vs Few-shot 的区别:
- zero-shot:
"把这句话分类成 7 类之一,只输出 JSON"------ 模型"知道"要分类,但不知道你期待的 JSON 长什么样,容易加解释/换字段名;- Few-shot:规则后附 6 组
文本 → {"category":...,"confidence":...,"reason":...}示例 ------ 模型照着示例的格式抄,输出稳定、字段名分毫不差。
4.2 本项目的 Few-shot 效果(实测对比)
L10 文本分类(规则 + 6 组样例),4 个输入全部命中:
| 输入 | 输出(模型返回的 JSON) | 是否命中 |
|---|---|---|
| 北京今天天气怎么样 | {"category":"weather","confidence":0.98,...} |
✅ |
| 写一个冒泡排序的Java代码 | {"category":"code","confidence":0.99,...} |
✅ |
| 帮我翻译成英文:谢谢 | {"category":"translate","confidence":0.97,...} |
✅ |
| 介绍一下黑洞是什么 | {"category":"knowledge","confidence":0.96,...} |
✅ |
L9 代码生成 (3 组「需求→代码」示例):生成的 OrderCleanTask 与示例结构高度一致(@Slf4j / @Scheduled / try-catch / 类注释)------模型在模仿示例的"规范感",而不是凭空发挥。
💡 无 Few-shot 会怎样:模型可能输出 Markdown 包裹、带解释、格式漂移;Few-shot 后输出稳定、可直接解析。这正是"给模型样例提升准确率"的价值。
4.3 样例库(统一管理)
java
// chat/prompt/FewShotExamples.java ------ 所有 Few-shot 示例集中管理
public static List<String> codeGenExamples() { ... } // L9:3 组 需求→代码
public static List<String> classifyExamples() { ... } // L10:6 组 文本→JSON
样例设计要点(技术细节):
- 覆盖全类别 :L10 的 6 组样例覆盖 6/7 类(weather/translate/code/tool/knowledge/chat),只留
other让模型"兜底猜"------如果所有类别都有正例,模型对未见类型会硬套已有类别;留一个无样例类别可显著提升"我不认识"的判断能力; - 样例即契约:样例就是输出格式的"活 Schema",改格式只改样例,模型会自动跟随;
- 小模型依赖度高:4B 模型对样例几乎"逐字模仿",所以样例本身必须符合规范(比如代码样例必须真能编译),否则会"学会坏习惯"。
🧱 五、Prompt 分层设计(人设 + 规则 + 业务 + 样例)
5.1 四层定义(PromptLayer 枚举)
| 层 | 作用 | 放哪里 | 变不变 |
|---|---|---|---|
| 🧍 人设层(PERSONA) | 设定角色/专业背景/语气 | SystemMessage | 固化 |
| 📏 规则层(RULES) | 输出格式、边界、禁止项 | SystemMessage | 固化 |
| 📄 业务层(BUSINESS) | 本次任务的动态输入 | SystemMessage 末尾(本项目) | 每次变 |
| 🧪 样例层(EXAMPLES) | Few-shot 示例,示范理想输出 | SystemMessage | 固化 |
ℹ️ 关于业务层放哪里 :经典分层是"业务层放 UserMessage",但本项目为了同一 session 的多轮记忆不被割裂 (UserMessage 里如果既有业务层又拼历史,记忆会重复),把渲染好的业务层追加在 SystemMessage 末尾 (
system + "\n\n【业务层】\n" + business),历史消息只存原始用户输入与模型回答。两种放法都能跑,本项目选择是基于记忆实现的取舍,已在类注释说明。
5.2 为什么这样分
- 固化的放系统层(人设/规则/样例):每次请求重复使用,模型始终"在正确的框架下回答";
- 动态的放用户层(业务输入):与固化知识隔离,职责清晰;
- 可插拔:加人设不动规则、换样例不动业务------模板可组合、可调试(哪层出问题改哪层)。
5.3 拼装实现(PromptTemplateBuilder)
java
// 系统层 = 人设 + 规则 + 样例(按 PromptLayer 顺序,逐层日志)
String system = promptBuilder.buildSystem(persona, rules, examples);
// 业务层 = 模板 + 动态变量(PromptTemplateStore 渲染)
String user = promptBuilder.buildBusiness(businessTemplate, Map.of(...));
// L9/L10:业务层并入 SystemMessage 末尾;历史消息 = 多轮记忆
String fullSystem = system + "\n\n【业务层】\n" + user;
List<ChatMessage> messages = new ArrayList<>();
messages.add(SystemMessage.from(fullSystem));
messages.addAll(sanitizeHistory(memory.messages()));
⚠️ 注意 :把整段模板全塞进 UserMessage 也能跑,但分层后系统层可复用、业务层可替换 ,且日志里能清晰看到两层内容(
PromptTemplateBuilder的buildSystem/buildBusiness已逐层打印)。
💾 六、Prompt 缓存、复用、统一管理
6.1 统一管理:模板集中定义
- 模板文本 :集中在
chat/prompt/PromptTemplates.java(常量类,唯一事实来源); - 样例 :集中在
chat/prompt/FewShotExamples.java; - 渲染入口 :统一走
PromptTemplateStore.render(...)/PromptTemplateBuilder,不散落在 Handler。
6.2 缓存:编译一次、多次使用
java
private final Map<String, PromptTemplate> cache = new ConcurrentHashMap<>();
// computeIfAbsent:模板首次出现才编译,之后命中缓存
PromptTemplate template = cache.computeIfAbsent(templateText, PromptTemplate::from);
技术细节 :ConcurrentHashMap.computeIfAbsent 是原子操作 ------多线程并发首次访问同一模板时只编译一次,其余线程复用结果;PromptTemplate::from 只是解析占位符(内存操作,微秒级),缓存的价值在于避免重复解析 + 模板对象复用 。日志里能直接看到 🆕 首次编译并缓存 → ♻️ 命中缓存复用 的切换。
6.3 复用:同一模板、不同变量
java
// 同一业务模板,两个请求 = 两条不同指令
render(CODE_GEN_BUSINESS, Map.of("requirement", "写定时任务", ...));
render(CODE_GEN_BUSINESS, Map.of("requirement", "写 REST 接口", ...));
⚙️ 七、L9 / L10 实现原理(说清楚"它是怎么工作的")
7.1 L9 · Prompt 代码生成器(prompt-code)的实现原理
一句话 :把「用户自然语言需求」通过四层模板 + 3 组 Few-shot 翻译成「规范 Java 代码」的约束性指令,交给模型在**低温度(0.4)**下续写。
一条请求内部发生了什么(时序):
用户输入需求
│
▼
① 记忆写入:本轮用户消息 → ChatMemory(sessionId) 【多轮上下文的基础】
│
▼
② 人设层:默认架构师人设(前端 system 已置灰,模板统一管理)
│
▼
③ 分层拼装:buildSystem(人设 + 规则 + 3组样例) → 系统层 【固化知识】
│
▼
④ 业务层渲染:buildBusiness(CODE_GEN_BUSINESS, {requirement, language, framework})
│ → {{占位符}} 被实际需求替换 【动态输入】
▼
⑤ 消息组装:SystemMessage(系统层+业务层) + 历史消息 【记忆拼上下文】
│
▼
⑥ 参数应用:无自定义参数 → temperature=0.4;有 → 全部按用户配置
│
▼
⑦ 模型续写:qwen3:4b 在约束下生成代码(模仿样例的规范感)
│
▼
⑧ 回答写回记忆:非空回答 → AiMessage 入记忆 【供下一轮引用】
三个关键设计决策:
| 决策 | 原因 |
|---|---|
| temperature=0.4(默认) | 代码生成要确定性:温度太低(0.1)可能过度保守、模板化;太高(0.7+)会引入随机变体(变量名飘、结构漂移)。0.4 是"稳中带活"的平衡点。 |
| 3 组 Few-shot 全部入系统层 | 模型"看到"三组「需求→完整代码」后,会把输出格式(package/注解/@Slf4j/异常处理/注释)当作必须模仿的模板,而不是靠规则逐条约束。 |
| system 置灰锁定(前端) | L9 的人设层是分层教学的核心演示,若用户随意改 system,规则层/样例层的配合会被打破(例如用户把 system 改成"简短回答"),代码质量不可控。因此前端参数弹窗对 system 置灰不可勾选 + 橙色提示("人设层由 Prompt 模板统一管理");后端保留读取逻辑仅为兼容。 |
7.2 L10 · Prompt 文本分类器(prompt-classify)的实现原理
一句话 :把「任意用户文本」用7 类约束 + 6 组 JSON 样例压成一条"只许输出 JSON"的指令,模型在**极低温度(0.1)**下续写出标准分类结果。
用户文本
│
▼
① 记忆写入(同 L9)
▼
② 人设层:精准文本分类器(system 同样置灰锁定)
▼
③ 分层拼装:buildSystem(人设 + 7类规则 + 6组样例)
▼
④ 业务层:CLASSIFY_BUSINESS "待分类文本:{{text}}" ← 用户输入
▼
⑤ 消息组装 + ⑥ 参数应用(默认 0.1)
▼
⑦ 模型输出:{"category":"weather","confidence":0.98,"reason":"询问北京天气"}
为什么 L10 比 L9 温度更低(0.1) :分类是判别任务,需要每次结果稳定一致(同一句话 10 次调用应返回同一个类别);温度 0.1 让采样几乎退化为贪心解码,确定性最大化。
输出契约(规则层 + 样例层双重保证):
json
{"category":"<weather|translate|code|tool|knowledge|chat|other>","confidence":0.0-1.0,"reason":"<一句话理由>"}
category:7 类枚举,程序可直接映射到处理链路;confidence:置信度,供下游阈值判断(如 <0.7 转人工/二次确认);reason:可解释性,方便人工复核与日志审计。
7.3 两者的共同架构(为什么说"同一套代码")
L9/L10 都继承 AbstractChatHandler,只覆写三个钩子:buildMessages(组装消息)/ buildRequest(应用参数)/ afterChat(写回记忆)。差异只有模板内容与默认温度 ------这就是"分层 + 模板化"带来的复用收益:新增一个 Prompt 场景 ≈ 写一套模板 + 一个极薄的 Handler,其余全部复用。
右侧配置能力 (L9/L10 已完整继承 L7/L8):多轮记忆(sessionId 隔离)、全参数自定义弹窗(system 置灰锁定,其余全量可调)、我的工具箱面板(勾选状态随请求携带,但本模式不执行工具调用------避免工具返回内容污染代码/JSON 输出)。
🛣️ 八、L9 / L10 实现过程(从零到可用的 8 步)
8.1 落地步骤清单
| 步骤 | 做什么 | 改/新增文件 | 核心代码 |
|---|---|---|---|
| ① | ChatMode 加 2 个枚举(含 requiresSession/customizable 元数据) |
enums/ChatMode.java |
PROMPT_CODE(...) / PROMPT_CLASSIFY(...) |
| ② | 定义四层枚举 | chat/prompt/PromptLayer.java |
PERSONA / RULES / BUSINESS / EXAMPLES |
| ③ | 模板常量(唯一事实来源) | chat/prompt/PromptTemplates.java |
人设/规则/业务模板常量 |
| ④ | 样例库(Few-shot) | chat/prompt/FewShotExamples.java |
codeGenExamples() ×3 / classifyExamples() ×6 |
| ⑤ | 渲染注册表(编译缓存) | chat/prompt/PromptTemplateStore.java |
ConcurrentHashMap + computeIfAbsent |
| ⑥ | 分层组装器 | chat/prompt/PromptTemplateBuilder.java |
buildSystem / buildBusiness |
| ⑦ | 两个 Handler(继承抽象类,覆写 3 钩子) | chat/handler/PromptCodeChatHandler.java、PromptClassifyChatHandler.java |
@Component implements 钩子 |
| ⑧ | 前端联动验证 | GET /api/chat/modes |
零改动自动出现(前端按元数据渲染右侧三区块) |
8.2 一次请求的服务端日志(完整"实现过程"实拍)
以 L10 一次分类请求为例,服务端日志完整呈现每一步:
🧠 记忆:session=log-1 已写入本轮用户消息(长度=7),当前记忆共 1 条
🧍 人设层 → 你是一个精准的文本分类器,擅长把用户输入归类到预设类别。
📏 规则层 → 对待分类文本进行分类,类别只能是以下 7 类之一...
🧪 样例层 → 共 6 条 Few-shot 示例
样例 1:文本:北京今天天气怎么样
...(6 条逐条打印首行)
🔗 系统层拼装完成:长度=814 字符(人设+规则+样例)
📄 渲染模板 | 🆕 首次编译并缓存 | 变量=[text] | 模板长度=14 | 渲染后长度=13
📄 业务层填充后 → 待分类文本:北京天气怎么样
📋 消息组装完成:SystemMessage(系统层 835 字符) + 历史消息 1 条 | 消息总数=2
🎚️ 参数:未配置自定义参数 → 使用场景默认 temperature=0.1
🤖 ▶ 开始调用模型...
✅ 完成:{"category":"weather","confidence":0.98,...}(回答已写回记忆)
第二次调用同一模板时,日志会变成 📄 渲染模板 | ♻️ 命中缓存复用 | ... ------ 缓存命中的过程肉眼可见。
8.3 实现前后对比(代码量视角)
| 视角 | 实现前(字符串拼接) | 实现后(模板化) |
|---|---|---|
| 改需求文案 | 翻 Handler 改字符串 | 只改 PromptTemplates 常量 |
| 加样例 | 手拼字符串 | FewShotExamples 加一条 List.of(...) |
| 加新模式 | 新 Handler 里再拼一遍 | 复用 Store/Builder,只写模板 + 薄 Handler |
| 排查问题 | 看不出模型收到什么 | 日志逐层打印,一眼定位哪层有问题 |
🏢 九、L9 / L10 使用场景与真实企业案例
9.1 L9(代码生成器)适用场景
| 场景 | 说明 |
|---|---|
| CRUD/脚手架代码生成 | 需求 → Controller/Service/Mapper 骨架,团队统一风格 |
| 定时任务 / 工具类生成 | "写一个定时清理任务""写一个命名转换工具" |
| 测试用例生成 | 需求 → 单测骨架(配 Few-shot 样例即可扩展) |
| 代码审查辅助 | 生成"规范版"对照,给新人做 Diff 参考 |
| 内部开发平台 | 需求工单 → 预生成代码草稿,减少重复劳动 |
9.2 L10(文本分类器)适用场景
| 场景 | 说明 |
|---|---|
| 工单自动分派 | 客服工单 → 分类 → 自动路由到对应处理组 |
| 意图识别 / 对话路由 | 用户消息 → 意图 → 分发到检索/工具/人工等不同链路 |
| 内容审核初筛 | 文本 → 分类(广告/违规/正常)→ 进不同审核流 |
| RAG 查询路由 | 判断"知识问答"还是"闲聊",决定是否触发向量检索 |
| 舆情/评论分类 | 评论 → 正向/负向/中性,置信度低的转人工 |
9.3 企业真实案例(学习参考,非本项目部署)
案例 1 · 银行客服工单自动分类(L10 模式)
- 背景:某银行客服系统日均 2000+ 工单,原靠人工打标签,误分率高、流转慢。
- 落地:工单文本 → 模型预分类(7 类:账户/转账/贷款/信用卡/投诉/咨询/其他)+ 置信度 <0.8 自动转人工复核。
- 效果:分类准确率从人工的 ~85% 提升到模型+复核的 ~97%,平均流转时间缩短 40%,
confidence字段成为"是否转人工"的自动化开关。
案例 2 · 开发平台代码规范统一(L9 模式)
- 背景:某互联网公司 30+ 后端团队,代码风格不统一,Code Review 大量时间花在"改格式"上。
- 落地:把公司《Java 开发规范》写进 L9 的规则层 ,典型 Controller/Service 写法写进样例层,需求输入自动生成符合规范骨架。
- 效果:新项目脚手架生成从"半天"缩到"5 分钟",Review 中格式类评论下降约 60%。------这正说明分层模板的"规则层/样例层"是接入企业规范的最佳入口。
案例 3 · 智能客服意图路由(L10 + L8 组合)
- 背景:某电商智能客服,需要区分"查订单/问物流/退货/闲聊"。
- 落地:L10 先分类意图 → 命中"工具类意图"再走 L8 工具调用(查订单走订单 API)→ 闲聊直接话术回复。
- 效果:分类 + 工具的组合 避免了"所有问题都调 API"的资源浪费,高频问题机器人独立消化 70%+。本项目里 L10 的
tool类别正是为这种路由预留的。
案例 4 · RAG 知识库查询路由(L10 前置分类)
- 背景:企业知识库 RAG 系统,用户一半问题是"闲聊"。
- 落地:查询前先用 L10 分类,
knowledge才走向量检索,chat直接闲聊回复。 - 效果:省掉大量无效检索 ,检索成本下降、回答延迟降低------
knowledge/chat两个类别就是为这个场景设计的。
9.4 案例共性总结
企业落地 Prompt 的黄金三步 :① 用 L10 类分类器做"分流/路由/质检"(置信度转人工);② 用 L9 类生成器做"规范产出"(规则层 = 企业规范,样例层 = 最佳实践);③ 用分层模板把"企业规范"沉淀为可维护资产,改规范只改模板不改代码。
⚖️ 十、没有 L9 / L10 会怎样?(实现前后对比)
10.1 总体对比
| 维度 | 实现前(没有 L9/L10) | 实现后(有 L9/L10) |
|---|---|---|
| 代码生成 | 只能用通用聊天模式"碰运气",模型输出带 Markdown 包裹、解释、格式漂移,不能直接编译 | 规则层 + 3 组样例约束,输出规范可编译代码,temperature=0.4 保证稳定 |
| 文本分类 | 没有分类能力;或手写规则引擎/正则硬编码,维护成本高、覆盖差 | 7 类 Few-shot 分类 + 结构化 JSON + 置信度,准确率 90%+,改类别只改模板 |
| Prompt 管理 | 提示词散落在各 Handler 字符串拼接,改一处全链路翻找 | 模板集中 PromptTemplates 常量 + FewShotExamples 样例库,单一事实来源 |
| 缓存复用 | 每次请求重新拼字符串 | ConcurrentHashMap 编译缓存,🆕→♻️ 肉眼可见 |
| 多轮上下文 | 通用模式记忆不可控 | sessionId 隔离记忆,L9 可"改上一轮类名" |
| 参数控制 | 固定参数,无法按场景调 | 全参数自定义 + 场景默认温度(L9=0.4 / L10=0.1) |
| 可观测性 | 看不到模型收到了什么 | 日志逐层打印(人设/规则/样例/业务/参数),一次请求 = 完整实现过程 |
| 人设可控性 | system 随意改,教学演示被破坏 | system 置灰锁定(模板统一管理),其余参数全量可调 |
10.2 具体场景前后对比
场景 A:让模型生成一段代码(L9)
| 实现前(普通对话) | 实现后(L9) | |
|---|---|---|
| 输入 | "写个定时任务" | 相同输入 |
| 典型输出 | 可能输出:`以下是代码:` + Markdown 包裹 + 代码不完整 + 带解释 |
直接输出完整 OrderCleanTask,无包裹、可编译、含 @Slf4j/@Scheduled/try-catch |
| 多轮 | 下一轮上下文丢失 | "把类名改成 CleanupTask" → 正确改名 |
场景 B:让模型判断用户问什么(L10)
| 实现前(规则/正则) | 实现后(L10) | |
|---|---|---|
| 做法 | 正则匹配"天气/翻译/代码"关键词,漏一个改一次代码 | 模板改一行类别描述,样例加一组 |
| 未知输入 | 正则全 miss → 兜底错误分类 | 模型泛化 + confidence 低值自动转人工 |
| 输出 | 裸字符串,程序难处理 | 标准 JSON:{category, confidence, reason} 直接消费 |
场景 C:排查一次"模型答歪了"
| 实现前 | 实现后 | |
|---|---|---|
| 排查方式 | 猜模板哪里写错、加日志重跑 | 服务日志直接展示完整链路:记忆写入→四层拼装→占位符替换→参数→回答,定位到具体层 |
⚠️ 边界提醒:L9 基于 4B 小模型,能输出规范骨架,但复杂业务逻辑质量有限------这是 Few-shot 价值的最佳演示,生产建议换更大模型(DeepSeek V4 Pro 等,本项目多模型架构直接可切)。
📚 十一、项目文件速查
| 文件 | 职责 | 对应知识点 |
|---|---|---|
chat/prompt/PromptLayer.java |
四层枚举(人设/规则/业务/样例) | ⑤ 分层设计 |
chat/prompt/PromptTemplates.java |
模板常量(统一目录的单一来源) | ⑥ 统一管理 |
chat/prompt/FewShotExamples.java |
Few-shot 样例库 | ④ Few-shot |
chat/prompt/PromptTemplateStore.java |
模板注册表 + 编译缓存 | ⑥ 缓存/复用 |
chat/prompt/PromptTemplateBuilder.java |
分层拼装 + 变量渲染 | ⑤ 分层 + ③ 占位符 |
chat/handler/PromptCodeChatHandler.java |
L9 代码生成器 | 案例 ② |
chat/handler/PromptClassifyChatHandler.java |
L10 文本分类器 | 案例 ③ |
enums/ChatMode.java |
新增 2 个模式枚举 | 接入 |
AbstractChatHandler |
模板方法基类(buildMessages/buildRequest/afterChat 钩子) |
复用架构 |
🚀 下一步可做的增强:① 分类结果在后端做一次 JSON 解析校验(剥离 Markdown 包裹后
ObjectMapper反序列化,失败降级"other");② 代码生成模式增加语言/框架下拉参数(对应 3.3 节扩展方向);③ 用 PromptTemplate 重构 L3 template 模式(把字符串拼接换成模板引擎,教学更统一);④ 更多 Few-shot 场景(JSON 抽取、情感分析、SQL 生成)。