三条提示词约束,压住 RAG 助手的幻觉
RAG 教程里最常见的说法是「检索增强能消除幻觉」。做过就知道这话只对一半:检索给了模型事实来源,但模型没有义务只用你给的事实。你不拦着,它照样把上下文里没有的东西说得头头是道——毕竟它的天职是「把话补全」,不是「忠于来源」。
这个博客助手上线前我专门测过幻觉:问一些博客没写过的技术问题,看它是坦诚说不知道,还是开始编。最后压住幻觉靠的不是换模型,是三条提示词约束加一个代码分支。这篇逐条拆。
先看真实的 SYSTEM_PROMPT
不是教学示例,是线上正在跑的这份(blog-agent/app/routes/chat.py):
SYSTEM_PROMPT = """你是 kalpacode 博客的 AI 助手。请基于以下检索到的博客内容回答用户问题。
回答要求:
1. 仅基于提供的上下文,不要编造未在上下文中出现的信息
2. 如果上下文不足以回答问题,坦诚告知"博客中暂无相关内容"
3. 用中文回答,简洁清晰
【检索上下文】
{context}"""
三条要求,每一条对应一种具体的翻车方式。
第一条:「仅基于上下文,不要编造」——防自由发挥
不加这条时,模型的典型表现是**「检索到的 + 它以为的」混着输出**。比如检索结果里我写了「ChromaDB 用 cosine 距离」,模型会顺嘴补一段「余弦相似度的数学公式是……」,公式本身没错,但那不是博主写的,语气还一模一样,读者根本分不出哪句有出处。
对一个以「博主观点」为卖点的助手来说,这种无害的补充就是幻觉——它稀释了「这句话博主真的写过」这个承诺。所以第一条约束的本质不是防错,是划清出处边界:回答里出现的每个论断,都必须能在检索上下文里找到。
实测加一个细节有效:约束里写明「未在上下文中出现的信息」这个判定标准,比泛泛的「不要编造」更可执行。模型需要一个能检查的判据,而不是一个道德要求。
第二条:「不足就坦诚告知」——给模型一条退路
只堵不给路,约束会失效。如果只说「不许编」但不告诉它「答不了的时候该怎么办」,模型面对上下文不足的问题,大概率还是硬答——因为「生成一个回答」是它被训练出来的默认动作,沉默不是。
第二条就是那条退路:把「承认不知道」变成一个合法的、有固定话术的输出。「坦诚告知博客中暂无相关内容」这句话给了模型一个低成本的服从路径——比起硬编一个可能穿帮的答案,说一句「没有」明显更容易被优化目标接受。
这条还有一个隐性收益:它把「博客没写」本身变成了一种有用信息。用户问「K8s 怎么部署」,得到「博客中暂无相关内容」,潜台词是博主没写过这个主题——这比一个半对半错的通用回答更符合这个助手的定位。
第三条:「中文回答,简洁清晰」——防风格漂移
这条看起来最不重要,实际作用很实在。没有它,模型偶尔会整段引用英文资料、或者把 50 字能答完的问题铺成 500 字小论文。前者在检索上下文里混有英文内容时高发,后者是模型的默认啰嗦。风格漂移不算幻觉,但对「简洁站内问答」这个产品形态来说同样是需要压住的行为。
第二道保险:检索为空时,根本不调模型
提示词约束管得住「模型生成时」,但有一个场景提示词天然管不了:检索结果为空。此时 {context} 是空字符串,模型拿到的是一个「请基于以下内容回答(内容:无)」的荒谬请求——你把它逼到了墙角,它除了编没有别的出路。
所以代码里有一个独立分支,检索为空时绕过 LLM 直接返回固定文案(no_result_stream):
if not results:
async def no_result_stream():
yield {"data": json.dumps({
"content": "博客中暂无相关内容,你可以换个问题试试。",
"done": True, "sources": [], ...
}, ensure_ascii=False)}
return EventSourceResponse(no_result_stream())
这个分支的设计哲学值得单独说:防幻觉要软硬结合。提示词约束是软保险——它影响模型的倾向,但模型的服从是概率性的,服从率再高也不是 100%。代码分支是硬保险——空结果这个场景下,生成内容根本不经过模型,幻觉的可能性被结构性消除,不是被压低。
两者的分工原则:能用代码消除的路径,不要留给提示词去防。「上下文不足时该怎么办」这种连续光谱上的问题(检索到了但不够全面)只能靠软约束;「检索结果为零」这种离散的、可判定的状态,必须走硬分支。顺手还有个副产品:这个分支不调 LLM,响应零延迟、零 token 成本。
不加约束时长什么样
做个对照实验就明白了。把 SYSTEM_PROMPT 换成一句「你是助手,请回答」,然后问几个博客没覆盖的问题,典型翻车有三种:一是直接开编,还编得像模像样地引用「博主的文章」;二是把通用知识套上第一人称,「我在这篇文章里写过」——而我根本没写过;三是上下文不足时开始反问和跑题,用废话填充。三种翻车的共同点是模型在尽力满足「给出一个像样回答」的默认目标,因为没人给它别的目标。三条约束的本质,就是把这个默认目标替换掉。
复盘
这套防幻觉体系的成本低得不成比例:三条约束几十个字,一个分支十行代码。但它要求你想清楚一件教程不太提的事:幻觉不是模型的 bug,是无约束条件下的正常输出。你的系统里每一个「模型可能无话可说」的路径,都得提前决定好:给它退路(软约束),或者干脆不给它说话的机会(硬分支)。两样都做了,幻觉就从「会不会出」的问题,变成「多久被用户抓到一次」的运营问题——后者是可以接受的,前者不行。