第11讲:Prompt 工程与 Profile 系统¶
上一讲:RAG Pipeline 主流程深度解析 下一讲:FastAPI 与异步 Web 框架
本讲目标¶
- 理解 Prompt Profile 的概念和设计动机
- 掌握 System Prompt 的编写原则(身份、边界、约束)
- 理解问题类别与 Prompt 模板的映射关系
- 理解为什么让 LLM 自行判断模板会引入不稳定性
本讲地图¶
本图对应本讲功能闭环,展示从输入到本讲交付物的主干路径。节点与主项目代码文件和函数保持一致,后续章节消费的能力只作为交付边界出现。
图 1:第 11 讲功能闭环地图¶
flowchart TD
C11_PROFILE["Profile 结构<br/>PromptProfile"]
C11_TEMPLATES["模板常量<br/>DEFAULT_*"]
C11_SELECT["Profile 选择<br/>build_answer_prompt_profile()"]
C11_SCENARIO["场景变量<br/>_scenario_prompt_context()"]
C11_PROMPT["用户 Prompt<br/>prepare_answer()"]
C11_ANSWER["可靠回答<br/>_finish_with_single_answer()"]
C11_DIAG{{"诊断信息<br/>retrieval_info['prompt_profile']"}}
C11_PROFILE --> C11_SELECT
C11_TEMPLATES --> C11_SELECT
C11_SCENARIO --> C11_SELECT
C11_SELECT --> C11_PROMPT
C11_PROMPT --> C11_ANSWER
C11_ANSWER --> C11_DIAG
style C11_PROFILE fill:#F8FAFC,stroke:#64748B,stroke-width:2px
style C11_TEMPLATES fill:#DBEAFE,stroke:#2563EB,stroke-width:2px
style C11_SELECT fill:#F5F3FF,stroke:#7C3AED,stroke-width:2px
style C11_SCENARIO fill:#FEF3C7,stroke:#D97706,stroke-width:2px
style C11_PROMPT fill:#DBEAFE,stroke:#2563EB,stroke-width:2px
style C11_ANSWER fill:#F5F3FF,stroke:#7C3AED,stroke-width:2px
style C11_DIAG fill:#DCFCE7,stroke:#16A34A,stroke-width:2px
节点与代码对齐¶
| 节点 | 对齐文件 | 函数/对象 | 本章职责 |
|---|---|---|---|
| Profile 结构 | qa_core/prompts/profiles.py |
PromptProfile |
承载 system_template、user_template 和 profile_name。 |
| 模板常量 | qa_core/prompts/templates.py |
DEFAULT_* |
定义默认回答模板和上下文格式。 |
| Profile 选择 | qa_core/prompts/selector.py |
build_answer_prompt_profile() |
按问题类别、意图和场景选择回答口径。 |
| 场景变量 | qa_core/prompts/selector.py |
_scenario_prompt_context() |
把 assistant_name、business_domain、support_contact 注入模板。 |
| 用户 Prompt | qa_core/pipeline/steps.py |
prepare_answer() |
把历史、问题和证据填入选定 Profile。 |
| 可靠回答 | qa_core/pipeline/rag.py |
_finish_with_single_answer() |
有证据则流式生成回答并带来源,无证据则明确信息不足。 |
| 诊断信息 | qa_core/pipeline/rag.py |
retrieval_info['prompt_profile'] |
把 Prompt 档位和预览写入诊断信息。 |
第一部分:前置知识 — Prompt 工程基础¶
1.1 什么是 Prompt 工程¶
Prompt 工程是设计和优化发送给 LLM 的指令文本的过程。
一个完整的 Chat Prompt 通常由两部分组成:
1.2 为什么 System Prompt 很重要¶
没有 System Prompt 的 LLM 是一个"通用聊天机器人",它可能会: - 用搞笑的口吻回答严肃的企业制度问题 - 在没有资料的情况下自信地编造答案 - 忘记自己的专业领域,回答任何问题
有了明确的 System Prompt,LLM 成为一个"有角色的专业助手"。
第二部分:Prompt Profile 系统设计¶
2.1 什么是 Prompt Profile¶
Prompt Profile 是一组预设的 System Prompt + User Prompt 模板,针对不同类型的问题使用不同的回答策略。
当前代码中共有 8 个最终回答档位:faq_answer、knowledge_answer、follow_up、pricing_guard、compliance_guard、troubleshooting_steps、source_bound_summary、default_answer。其中前 3 个按意图兜底选择,后 4 个按问题类别优先选择,default_answer 只用于未知意图或新增意图兜底。
2.2 模板注册¶
2.3 场景变量注入¶
注意模板中的 {assistant_name}、{business_domain}、{industry} 等变量。这些变量在运行时从场景配置注入:
这样同一套 Prompt 模板可以用于所有 8 个场景,只需配置不同的业务身份。
第三部分:问题类别的严格模板¶
3.1 费用类(Pricing)模板¶
3.2 合规类(Compliance)模板¶
3.3 问题类别与模板的映射¶
3.4 选择优先级¶
flowchart TD
Input(["query + intent + scenario"]) --> Cat["🔍 infer_question_category()"]
Cat --> CatCheck{"类别匹配?"}
CatCheck -->|"pricing"| P1["💰 PRICING_PROFILE<br/>严格金额约束<br/>不得估算承诺"]
CatCheck -->|"compliance"| P2["⚖️ COMPLIANCE_PROFILE<br/>最严格<br/>不得自行判断合规性"]
CatCheck -->|"troubleshooting"| P3["🔧 TROUBLESHOOTING<br/>扩大搜索<br/>SOP+原因+步骤"]
CatCheck -->|"summary"| P4["📊 SUMMARY_PROFILE<br/>多段资料综合<br/>归纳总结"]
CatCheck -->|"无匹配"| IntentCheck{"意图匹配?"}
IntentCheck -->|"FAQ_QUERY"| P5["📋 FAQ_ANSWER<br/>标准答案模板"]
IntentCheck -->|"KNOWLEDGE_QUERY"| P6["📚 KNOWLEDGE_ANSWER<br/>综合分析模板"]
IntentCheck -->|"FOLLOW_UP"| P7["💬 FOLLOW_UP<br/>结合历史模板"]
IntentCheck -->|"无匹配"| P8["📄 DEFAULT<br/>通用安全模板"]
P1 --> Inject["💉 注入场景变量<br/>assistant_name<br/>business_domain<br/>industry<br/>support_contact"]
P2 --> Inject
P3 --> Inject
P4 --> Inject
P5 --> Inject
P6 --> Inject
P7 --> Inject
P8 --> Inject
Inject --> Output(["PromptProfile<br/>system_template + user_template"])
style P1 fill:#FEF3C7,stroke:#F59E0B,stroke-width:2px
style P2 fill:#FEF2F2,stroke:#DC2626,stroke-width:2px
style CatCheck fill:#EFF6FF,stroke:#2563EB,stroke-width:2px
第四部分:为什么不用 LLM 选择模板¶
这是一个值得讨论的设计决策。
方案 A(不使用):让 LLM 每次自己判断该用哪个模板。
方案 B(本项目采用):确定性规则选择。
选择方案 B 的原因:
-
检索策略和 Prompt 模板必须一致:如果检索时判断为 FAQ_QUERY(FAQ 优先),但 Prompt 时 LLM 判断为 knowledge_answer(综合分析),就会出现"只召回了 FAQ 但要求综合分析"的不匹配。
-
减少一次 LLM 调用:每次生成答案前再让 LLM 选一次模板,增加延迟和成本。
-
确定性 > 灵活性:在高风险场景(费用、合规),我们希望模板选择是 100% 确定的,不能依赖 LLM 的"判断"。LLM 可能在一次调用中正确选择了合规模板,下一次却用了默认模板。
第五部分:System Prompt 编写原则¶
从本项目的 Prompt 设计中可以总结出几个原则:
5.1 身份明确¶
5.2 行为边界清晰¶
5.3 对高风险场景做显式约束¶
5.4 提供失败兜底行为¶
本讲实践闭环¶
| 项目 | 内容 |
|---|---|
| 本讲类型 | 项目实现 |
| 实践产物 | prompts/profiles.py、prompts/selector.py 和场景化 Prompt Profile |
| 是否进入最终项目 | 是 |
| 验收方式 | 不同问题类别命中不同模板,高风险问题进入安全约束模板 |
| 后续落点 | 第 10 讲生成阶段使用,第 17/19 讲观察答案质量和 trace |
通过标准:Prompt 选择有明确优先级,模板变量能从场景配置注入,回答边界可控。
本讲从 0 到 1 实现闭环¶
这一讲要完成的是“Prompt 可治理”,不是写一个越来越长的大提示词。实现顺序如下:
flowchart TD
Q["用户问题"] --> Intent["意图识别结果<br/>FAQ / Knowledge / Follow-up"]
Q --> Category["问题类别<br/>pricing / compliance / default"]
Category --> CatHit{"是否命中<br/>高优先级类别?"}
CatHit -->|"是"| Guard["选择类别保护模板<br/>费用/合规/风险"]
CatHit -->|"否"| IntentPick["按意图选择模板"]
IntentPick --> Profile["PromptProfile<br/>system + user + reason"]
Guard --> Render["注入场景变量<br/>assistant_name / domain / fallback"]
Profile --> Render
Render --> Messages["生成 LLM Messages"]
- 先把回答模板抽象成
PromptProfile,每个 profile 说明适用场景和选择原因。 - 再把 FAQ、知识问答、追问、高风险费用/合规问题拆成不同 profile。
- 然后实现确定性的选择器:先看问题类别,再看意图,最后才用默认模板。
- 最后把场景配置中的助手名称、业务领域、兜底话术注入模板。
实现完成后,相关代码结构应该是下面这张图:
flowchart LR
subgraph Core["qa_core"]
subgraph Prompts["prompts"]
Profiles["profiles.py<br/>PromptProfile<br/>模板注册"]
Selector["selector.py<br/>build_answer_prompt_profile()<br/>确定性选择"]
end
subgraph Pipeline["pipeline"]
Steps["steps.py<br/>构建 LLM messages"]
end
end
subgraph Tests["tests"]
PromptTest["test_retrieval_and_prompt.py<br/>类别模板<br/>安全约束"]
end
Profiles --> Selector
Selector --> Steps
PromptTest -. 验证 .-> Selector
来源:真实代码逻辑压缩版,对应 qa_core/prompts/profiles.py。
选择逻辑必须是确定性的。费用、合规、合同风险这类高风险类别不能交给 LLM 自己判断,否则一次误判就可能造成错误承诺。
来源:真实代码节选,见 qa_core/prompts/selector.py::build_answer_prompt_profile()。
场景变量不要写死在 Prompt 里。8 个业务场景共用一套模板,只把 assistant_name、business_domain、fallback_contact 等配置注入进去。
来源:真实代码逻辑压缩版,对应 qa_core/prompts/selector.py::_scenario_prompt_context()。
验收时不要只看“能回答”,还要验证不同类别是否命中正确 profile。
来源:命令行验收,对应 tests/test_retrieval_and_prompt.py。
闭环验证重点:
| 验证项 | 输入条件 | 期望结果 |
|---|---|---|
| FAQ 模板 | intent=FAQ_QUERY |
命中 FAQ 直出模板 |
| 知识问答模板 | intent=KNOWLEDGE_QUERY |
命中知识回答模板 |
| 追问模板 | intent=FOLLOW_UP |
命中追问上下文模板 |
| 费用类保护 | category=pricing |
命中费用安全模板 |
| 合规类保护 | category=compliance |
命中合规安全模板 |
| 场景变量 | 切换业务场景 | assistant/domain 等变量正确注入 |
| 默认兜底 | 新增未知 intent | 使用 DEFAULT_PROMPT_PROFILE |
| 模板脱敏 | 调试信息 | as_dict() 只返回 name/reason,不暴露完整 prompt |
验收重点:Prompt 是一组可选择、可解释、可测试的业务回答策略,不是散落在代码里的长字符串。
重点掌握¶
| 优先级 | 内容 | 原因 |
|---|---|---|
| ★★★ 必会 | Prompt Profile 的概念和结构:name + system_template + user_template + reason,8 个最终回答档位 | 本项目的提示词管理方式 |
| ★★★ 必会 | Profile 选择优先级:问题类别(pricing/compliance/troubleshooting/summary)> 意图(FAQ_QUERY/KNOWLEDGE_QUERY/FOLLOW_UP)> 默认模板 | 确定性的选择策略 |
| ★★★ 必会 | 高风险问题类别的特殊模板:费用类(不得估算金额)、合规类(不得主观判断合规性) | 高风险场景的 Prompt 安全约束 |
| ★★ 理解 | 确定性选择 > LLM 判断的原因:检索策略和 Prompt 模板必须一致、减少一次 LLM 调用、高风险场景必须 100% 确定 | 重要的设计决策依据 |
| ★★ 理解 | 场景变量注入(_scenario_prompt_context):assistant_name、business_domain 等从 TOML 注入 | 同一套模板跨 8 个场景复用的机制 |
| ★★ 理解 | build_answer_prompt_profile() 的完整选择逻辑 | Profile 选择的代码实现 |
| ★ 了解 | System Prompt 编写原则:身份明确、行为边界清晰、高风险显式约束、提供失败兜底 | Prompt 工程通用技巧 |
| ★ 了解 | CATEGORY_PROMPT_PROFILES 和 PROMPT_PROFILES 两个字典的注册方式 | 了解模板组织方式 |
本讲小结¶
- Prompt Profile 是预设的 System/User Prompt 模板集合,针对不同问题类别使用不同策略
- 选择优先级:问题类别(费用/合规等)> 意图(FAQ/知识咨询)> 默认模板
- 场景变量注入使同一套模板可以跨 8 个场景复用
- 确定性选择 > LLM 判断:高风险模板(费用、合规)的选择必须是 100% 确定的
- 关键在于让 LLM 知道"能做什么"比"不能做什么"更难,所以约束性指令要明确
下一讲:FastAPI 与异步 Web 框架 — async/await、WebSocket、FastAPI 路由设计