从零到一搭建企业级智能问答系统:第2章 · 提示词工程管理

情况 2:更危险 ------ 内容里的占位符被意外替换

问: 等于该符号, 用户宣称: 用户发出的询问为, {query} 这个内容, 其具体所表达的含义是什么, 此句的意思是。

文档内容里的 {query} 可能被替换,造成内容错乱

2.6 兜底与校验:别让一次手滑打翻全站

兜底------渲染后检查是否还有未替换的占位符:

re

def (, **):

=

for key, value in .items():

渲染后检查是否还有未替换的占位符

= set(re.(r"\{(\w+)\}", ))

if :

.(f"模板存在未替换变量: {}")

生产环境降级(填空),开发环境应报错

校验------防止后台改错变量名:

等于, 一个包含"query", 其值为空, 另有两个空值, 还有"role的值"也为空的集合。

def (: str) -> tuple

bool, str

"""校验模板中的占位符是否合法(后台保存前调用)。"""

= found -

if :

False, f"未知变量: {},可用: {}"

True, "OK"

2.7 改造前后的账本第二部分小结 · 对照自查

到此地, 目标2达成------提示词已然「能存进、可改动、能退回」。然而搬至数据库的价值, 并非仅在「改起来便捷」, 而是在于其使你次日便修复了一个棘手的bug。此bug即为第三部分的主角色。

第三部分 · 第一类幻觉:不该检索时检索

这一部分成功获取目标3, 这还是全章最为违背直观感受的一处要点。简单一句话来透露剧情: 这个幻觉, 调节提示词是没有作用的。

3.1 现象:闲聊也编出「知识库腔」

用户曾问"你好", 用户亦问"你是谁", 系统皆会去检索知识库, 之后编造一段呈现"知识库风格的回答"。

用户:你好

系统, 依据文档的第 2.1 节, 你好!此设备具备支持多种通讯协议的能力...有悖常理。

3.2 根因:不是提示词的问题,是路由的问题

这是本章最关键的一点。

错误的诊断:"提示词不够强,要加更严格的约束。"

正确的诊断:系统在该走"直接回答"的时候走了"检索回答"。

你好被用户问了之后, 会被判定为, 然后去进行线索检索, 接着编造基于文档的回答。

↑ 根因在这里

对于只在提示词里添加约束来说, 假设若是"如果是闲聊就直接回答"这种情况, 所得出的效果具备一定局限性, 原因归之为: 一方面, 这个拥有获得一堆文档契机的模型, 常常会表露出"把这些文档加以运用"的倾向;再者, 随着约束强度增大, 模型在两个指令之间左右摇摆的可能性就越大。

正确的修法:在意图识别阶段就分流,闲聊根本不进检索分支。

跟 ch01 相对照, 上一章你所排除掉的是第 2 类幻觉, 即检索到了但截断没读进去, 而这一章所排除的是第 1 类, 也就是压根不该去检索。两类之中, 一类在于能不能看见, 另一类在于该不该去看, 这些都是工程层面出现的错误, 并非模型本身的问题。

3.3 修法: 分支

.py:882 ------ 三条分支

graph.s(

"",

self.,

"": "", # 简单查询 → 多轮检索

"": "", # 复杂查询 → 多智能体拆解

"": "", # 闲聊 → 直接回答(不检索!)

},

.py:925

graph.("", "") # 闲聊直答后直接出口

对于其节点而言,完全不存在检索这一环节, 在结构方面, 这般杜绝了"闲聊编造文档内容"的情况。

3.4 意图识别的实现(三代技术的第一代)

第 014 次提交时用的是快速分类(词表):

.py:2438

自定义方法, 该方法接收一个查询字符串作为参数且返回一个字符串。句末有标点符号。

"""快速分类:基于词表的规则判定。

这是三代意图识别技术里的第一代(详见 Ch11):

第一代 关键词/词表 ← 本章

第二代 LLM 分类

第三代 向量语义路由 ← 最终形态

"""

.py:979

def (self, state: ) -> dict:

"""意图识别节点 ------ 系统的分叉口。"""

.py:1158

def (self, state: ) -> str:

"""根据意图返回下一个节点名(必须命中 的键)。"""

这次所提交的, 有着 119 行改动之处, 其中 113 行存在于 .py 当中------这意味着"提示词管理"此次的改造, 直接促使幻觉修复获得了加速。这恰恰是 L2 能力的价值得以兑现的体现: 要是不存在版本号后台, 那么这 113 行提示词, 仍旧得走上「改代码 + 重启」的旧途径。

3.5 幻觉的四种类型:先分类,再下药

类型

表现

修法

章节

① 不该检索时检索

闲聊/OOS 也去查文档

路由分流

本章

② 检索到了但没读进去

文档在 里但被截断

/ 出口契约

Ch01、Ch12

③ 检索到了但理解错

模型误读数字/单位

约束 + 强模型

Ch20

④ 检索不到就编

查空也硬合成答案

验收门四态判定

Ch23

不少人一旦碰到幻觉便做调整提示词的举动, 但事实上唯有第③类适宜运用提示词予以解决, 要先进行分类, 而后再对症下药。

第三部分小结 · 对照自查

到此处, 目标3达成, 即模型知晓了「何时不应去查询」。然而, 本章尚有一笔轻易可处理的账需要结算, 那便是提示词属于文本资产, 注释同样属于文本资产。如此一来, 便进入了第四部分。

第四部分 · 另一类文本资产:注释

这一部分达成目标 4。提示词得加以管理, 注释同样如此------它们全数是「撰写出来供人和机器去看」的文本资产。不同之处仅在于: 提示词是写给模型看的, 注释是写给人看的。

4.1 四个等级:什么样的注释才有价值

第 002 次提交只改了 13 行,但确立了注释的标准:

4.2 一个模板:六要素注释

给第 958 行、第 010 次所提交的代码补了 1295 行注释(注释与代码比例为 135%), 以 MMR 算法当成示例, 最后形成的注释模板是。

.py:2590

看起来你提供的内容不太完整且格式有误, 不太能按照要求准确改写。请你检查并提供正确完整的内容。

"""

【辅助:MMR ( ) 重排序】

其职责在于, 做一种文档排序算法, 该算法要在相关性以及多样性之间达成平衡。

原理: ← ② 为什么需要

有多个文档, 其返回容易, 是内容相似的(来自同一段落的不同切片), 属于向量检索。

把所有内容都塞给 LLM 的话, 不仅会造成 token 空间的浪费, 还会让答案倾向于重复的内容。

MMR 同时优化两个目标: ← ③ 核心机制

  • 相关性():选与查询最相关的文档

  • 多样性():选与已选中文档不重复的新文档

算法(贪心): ← ④ 实现步骤

  1. 按距离排序,选最相关的作为第一个选中项

  2. 循环选择剩余项:

a. 计算它和已选中集合的最大相似度()

b. MMR 分数等于, λ 乘以相关性, 减去, (1 减去 λ)乘以冗余度。

c. 选 MMR 分数最高的加入选中集合

λ(=0.7)的含义: ← ⑤ 参数含义

  • 越接近 1 越看重相关性,越接近 0 越看重多样性

  • 0.7 是经验值,7:3 的权衡

参数:query / / k ← ⑥ 签名说明

返回:MMR 排序后的文档列表

"""

存在六个要素, 分别是职责, 之后是动机, 再之后是机制, 如果接着是步骤, 然后是参数, 最后是返回值。

4.3 判断准则:意外程度决定注释量

判据:代码的"意外程度"越高,注释越要多。

记忆的口诀是, 函数名要是能够阐述清楚明白的, 那就不要去写, 函数名到截止的那个范围的, 那就一定得去写, 注释仅仅只需写为什么, 而不要去写怎么做原因是怎么做是会发生变化的, 但是为什么是极少会产生变动的。

4.4 这笔投资的回报

时点

场景

注释的价值

第 011 次提交

加记忆系统,要改节点

不用重读实现,看注释就知道在哪插

第 028 次提交

修记忆 bug

注释写了"身份贯通"的设计前提

第 055 次提交

修 Bad Case #9

出口契约注释直接指出问题

第 069 次提交

改语义路由

路由节点注释说明三层降级设计

现在

写这份教程

80% 内容来自这些注释

以半天的投入, 收获持续于整个项目周期的回报。这一回的"非功能"提交, 是此番项目里投资回报率最高的状况。

第四部分小结 · 对照自查踩坑总表

走到此处, 四个目标皆已成功达成。将这一章里所遇到的难题罗列于一张表格之上------每一项都是切实亲身经历过的:

现象

根因

修法

占位符与内容花括号冲突

文档含 {...} 时 str.() 抛

内容里的花括号被当成占位符

白名单逐个替换(见 2.5)

缓存失效不及时

后台改了模板,服务还用旧的

缓存 key 没带版本号

key = f":{name}:v{}"

多实例缓存不一致

4 个 ,改了模板有的生效有的没生效

各进程独立内存缓存

Redis 主动失效 / 缩短 TTL / 版本号轮询

后台改错变量名

{query} 写成 {quer},全站问答报错

无校验

四道防线(校验 / 兜底 / 恢复默认 / 灰度)

注释腐烂

改了代码没改注释,注释描述三个月前的逻辑

注释写了「怎么做」

只写「为什么」+ 关键断言写成可执行代码

这样的一张表, 是值得进行截图并且好好保存下来的。心甘情愿地将这些坑, 一个接着一个地提前去填平,而这, 便是工程化AI与玩具demo之间的分水岭了。

学习目标回顾

回到开头的四个目标,逐一自查:

四个全过,这一章就没白读。最后送一句话带走:

「写好提示词」属于手艺范畴, 「管好提示词」则归为工程领域。手艺存在过时一说, 工程却并非如此, 提示词数量越多, 部署规模越大, 改坏后代价越高昂, 管理也就越发具有价值有其贵重之处。

而要将提示词管理好的第一步, 始终都是先要去承认, 提示词所起到的作用仅在于约束「回答的方式」, 并不会去判定「是否进行回答」, 至于回答与否, 那是关于路由相关方面的事情。

知识点卡片【知识点】提示词工程的三个层次

L3 测得出 ← 离线评测集 / A/B(证明"变好了")

↑ 依赖

L2 管得住 ← 版本 / 灰度 / 回滚 / 可视化

↑ 依赖

L1 写得好 ← 措辞 / few-shot / 思维链

L2 作为 L3 的前提究竟是为何呢? A/B 测试明确规定要求两组采用不一样的版本提示词。若进行硬编码操作, 那么就必须部署两个实例才能够实现对比, 然而该成本实在是极高。当进入数据库之后, A/B 仅仅只是依据流量比例来选取不同的内容。

【知识点】幻觉的第一类:不该检索时检索

具备一种特性, 那就是当用户提出有关闲聊或者OOS方面的问题之后, 系统会展开检索的动作, 进而去编撰成类似那种知识库风格般的回答。

为啥调提示词没效果呢? 是由于模型已然获取到了文档, 它会趋向于"运用"这些文档。你增添约束讲"闲聊就径直回答", 模型反倒会在两条指令之间来回摆动。

正确修法:在路由层分流 ------ 闲聊根本不进检索分支:

{"": ""} # 节点没有检索环节

从结构上杜绝,而不是从措辞上约束。

【知识点】注释的"意外程度"原则

代码越意外,注释越要多。

判定方式: 去设想一位跟你水准不相上下的共事者阅读这段编码, 他会困在哪一处地方? 于每一个困住的地方撰写注解。

反例(浪费):

def ():

"""根据 ID 获取用户。""" # 函数名已经说清楚了

正例(必要):

BM25 取负:系统内统一约定"距离越小越相似",

不是"越大越不相似 ", 而是"越大越相似", 这才是BM25原始分数的情况。要是漏掉负号, 就会致使排序出现完全反向的状况。

dist = -

记忆口诀:函数名能说清的,别写;函数名为止的,必须写。

练习题基础题

  1. 有人针对用户问"你好"后系统按照依据文档第 2.1 节回复"您好!本设备支持多种通讯协议"的情况, 说这是提示词不够强, 对此进行评价, 这个诊断。

答案

这个诊断是错的。根因不在提示词,在路由。

正确诊断:系统在该走"直接回答"的时候走了"检索回答"。

用户问"你好" → 判为 → 走检索 → 编造基于文档的回答

↑ 根因在这里

为什么调提示词效果有限?

基于已拿到一堆文档的模型情况, 在这里添加"如果是闲聊就直接回答"这样的约束条款后, 模型会面临指令 A(隐含着要依据文档进行回答)与指令 B(明确表示闲聊就依此直接回答)这两个相互冲突的指令。

模型会在两者间摇摆,表现不稳定。

正确修法:在意图识别阶段分流,闲聊根本不进检索分支:

graph.s("", , {

"": "", # 节点没有检索环节

})

从结构上杜绝,而不是从措辞上约束。

通用的原则是, 某个问题, 要是反复借助"加约束"这个办法都解决不了, 那就得问问自己, 具体是不是在错误的那一层次上面解决该问题呢?

提示词是"生成层"的手段,而这个问题出在"路由层"。

  1. 为什么渲染提示词模板时不能用 str.()?

答案

两个原因:

原因 1:文档内容含花括号 → 抛异常

等于, 配置示例冒号, 左花括号,双引号空字符串冒号 30, 逗号, 重试冒号 3, 右花括号, 单引号。

= "上下文:{}"

会把 {""...} 当成占位符

.(=) # : ''

原因 2:更危险 ------ 内容里的占位符被意外替换

等于由用户提出的问, 问的内容是, {query}的所表达的意思究竟是什么, #在文档内容当中实际含有{query}。

= "上下文:{}\n问题:{query}"

= .(=, query="心跳间隔")

在某些特别的渲染顺序之时, 里的带有特定条件的 {query} 极有可能会被进行替代, 进而致使内容陷入错乱的状况。

正确解法:白名单逐个替换

def (, **):

=

for key, value in .items():

= .("{" + key + "}", str(value))

只替换模板声明过的变量,内容里的其他花括号原样保留。

这种时候, 当把那 "不可控的输入" 也就是文档内容, 与 "可控的模板" 放在一块儿进行处理之际, 对于这个坑的本质而言, 一定要明确其边界, 这是必然的。

  1. 用于提示词工程的L1是什么, 用于提示词工程的L2是什么, 用于提示词工程的L3是什么, 为何用于提示词工程的L2会处于用于提示词工程的L3的前提状况呢?

答案

层次

内容

L1 写得好

措辞、few-shot、思维链 ------ 决定单次效果上限

L2 管得住

版本、灰度、回滚、可视化 ------ 决定能否持续优化

L3 测得出

离线评测集、A/B 测试 ------ 决定优化方向对不对

为什么 L2 是 L3 的前提?

有关于A/B测试的技术要求, 两组流量得选用不同版本的提示词, 并且能够分别对效果进行统计哎。

同样的道理, L3的"离线评测"也是依靠L2的。是因为这个评测需要批量去运行不同版本的提示词, 要是采用硬编码的方式, 那么每测试一个版本就都得重新进行部署。

第一点, L1起着决定上限的作用, 第二点, L2决定着能否持续逼近上限, 第三点, L3决定你是不是朝着正确方向前行。

进阶题

  1. 部署了4个服务, 后台更改提示词后, 有的示例生效了, 有的示例没生效, 给出三种解决方案以及取舍。

参考答案

原因在于, 每一个都具有独立的内存缓存, 并且, 后台改动的是数据库, 然而, 各个进程的缓存对此并不知悉。

方案 1:缩短缓存 TTL(最简单)

= 10

def get(name):

如果名字在缓存之中, 并且当前时间减去缓存时间小于某个值。

cache

""

重新加载

方案 2:Redis 集中缓存 + 主动失效(推荐)

def get(name):

= redis.get(f":{name}")

if is None:

= (name)

redis.set(f":{name}", )

def (name, ):

db.(...)

拿 redis 来说, (f":特定的那个 name") # ← 重点在于其中的关键部分: 主动产生失效的情况。

方案 3:版本号轮询(无 Redis 时)

def get(name):

=数据库操作对象.("从哪里当名字等于?时",名字)。

如果浏览器缓存中获取名为name的内容, 且该内容中获取空字符串的值, 其结果不等于冒号。

cache = {"": , "": (name)}

cache

""

推荐的组合是, 存在着能够实现毫秒级响应的本地缓存, 有着以秒级为单位的Redis版本号, 并且在后台得以进行修改时会主动执行DEL操作。

这个项目起始之时为单实例状态, 并未遭遇此问题。然而, 在第 033 次呈交之物被引入"多"的情况后, 这般设计成为了必然之需。

  1. 原来, 是在后台进行提示词修改操作的时候, 把那个变量名给打错了, 结果, 就致使线上所有的问答都出现了报错, 为此要设计四道防线。

参考答案

防线 1:编辑时校验(前端 + 后端)

= {"query", "", "", "role", ""}

def (: str) -> tuple

bool, str

found = set(re.(r"\{(\w+)\}", ))

= found -

if :

False, f"未知变量: {},可用: {}"

True, "OK"

前端方面, 当进行输入这个行为的时候, 下拉补全那些可以使用的变量, 后端呢, 在保存之前要进行校验, 要是不合法那就直接拒绝。

防线 2:渲染时兜底(不崩)

= set(re.(r"\{(\w+)\}", ))

if :

.(f"模板存在未替换变量: {}")

for var in :

= .("{" + var + "}", "")

存在这样一个关键决策, 即未替换变量究竟是选择"填空", 还是选择"报错"呢? 其开发环境呈现出严格报错的设定, 这能使问题立刻暴露出来。而生产环境则采用降级填空并伴有告警的方式, 以此保证服务处于可用状态。

防线 3:一键恢复默认

def (name):

= # 默认模板硬编码在代码里

db.(name, =, =+1)

cache.(name)

作兜底之用, 默认模板被硬编码于代码内, 而数据库里所存的仅仅是那 "当前生效版本"。

防线 4:灰度发布

全新的模板, 致使百分之十的流量, 去观察那五分钟内出现的错误率, 若情况正常便放大至百分之百, 要是出现异常则会自动回滚。

要搭配 Ch18 的评测系统, 这系统要能够迅即评判新的版本究竟是更佳, 还是状况不如往昔。

那种最小的、可以被使用的组合是, 防线 1(校验), 再加上防线 3(让其恢复到默认状态), 它能够将 90%的事故阻挡住, 而且所需要耗费的成本是很低的。

本项目实现了防线 1(部分)、2、3,未实现 4。

思考题

  1. 给本项目的958行代码, 写了1295行注释, 注释与代码比为135%, 这属于过度注释吗, 请给出判断依据。

参考答案

不是过度注释。三条判断依据:

依据 1:看代码类型

. py这是用于编排还有与算法混合而成的文件, 其中存在着拥有13个节点的那种 业务逻辑, 这逻辑是怎样的还得去说明职责以及输入输出情况, 这儿还有MMR、RRF、query等那样的算法, 这类算法具体又是怎样的还得去说明原理以及参数才行, 另外, 还有3个路由函数的分支策略, 这种策略背后的兜底理由是啥也需要去说明。

这类代码的"意外程度"普遍偏高,高注释密度合理。

比照那种情况来讲, 要是面临的是CRUD相对集中的.py文件形势而言, 135%这样的比例明显状态显著偏向过度行列范畴范畴之内了。

依据 2:看内容而非数量

1295 行注释的分布:

L0 复述代码 ~0% ← 没有浪费

L1 重复函数名 ~10% ← 少量冗余

L2 解释为什么 ~60% ← 有价值

L3 决策与陷阱 ~30% ← 高价值

90% 有实质信息量。

依据 3:看后续回报(事后验证最有力)

在后续的33天当中, 这1295行注释: 支撑了第011次提交(加记忆)的很快改动, 加记忆是此次提交的一项内容;支撑了第028次提交(修bug)很精准的定位, 但不是模糊定位, 就像给问题找到了精确坐标, 精准度非常高;支撑了第055次提交(Bad Case #9)的根本原因分析, 是要挖掘深层次底层逻辑性原因而不是表面原因那种剖析过程;能看出来的是, 它也支撑了这份教程的写作, 其中80%的教程内容是直接源自这样的注释, 不是间接或者通过其他因素辗转等来的。

如果当时省下这半天,后面要多花几十倍时间重读代码。

但确有可改进之处:

属于 L1 的些许注释存在像是"""保存历史节点""" 这样的情况, 这些是属于浪费范畴的, 理想的做法是针对简单节点撰写仅仅一句话, 且将篇幅留给复杂算法。

更好的做法:把关键断言从注释升级为可执行代码:

与其写注释

注意: 应在 区间

不如让代码自己校验

0

相关推荐
ocean21033 小时前
2025-2026年AI提效与实践大厂面试高频问题
人工智能·面试·职场和发展·提示词工程·ai提效
森山冶仁4 天前
治理知识库构建:用 RAG 把制度、文档、经验变成 AI 能力
人工智能·智能问答·rag·企业知识库·大模型落地·ai治理
承渊政道8 天前
不只会打字回复:Open-LLM-VTuber接入大模型,让AI能听、能说、还能看到屏幕
人工智能·语音识别·智能问答·cpolar·open-llm-vtuber·多模态 ai 助手
Komorebi_999919 天前
提示词工程
rag·提示词工程
TonyLee01724 天前
关于大模型LLM的应用技术栈简记
大模型·agent·提示词工程
AI大佬的小弟1 个月前
大模型名词精讲 03:Prompt
llm·prompt·提示词·few-shot·zero-shot·提示词工程·大模型名词精讲
TunerT_TQ1 个月前
GitHub深度工程评测:AI 系统提示词泄露知识库深度评测:6万星system_prompts_leaks情报资产背后,藏着什么?
人工智能·chatgpt·github·提示词工程·#valhalla静态工程审阅·systemprompt·大模型工程
weixin_439930642 个月前
图片转PPT的提示词参考
提示词工程
小贺儿开发2 个月前
Unity 知识库智能问答系统(RAG)
人工智能·unity·ai·大模型·智能问答·知识库·互动