大模型回答跑题是生成式应用中最常见也最棘手的问题之一。表面上看,是模型没有理解用户意图;实际上,当上下文窗口增长到几千甚至几万token时,Transformer架构的注意力机制会被大量历史信息和无关片段分散权重,模型很难持续聚焦在最初的指令上。单纯的解码参数调整,比如降低温度或者提高重复惩罚,只是让输出更保守,并不能真正把回答拉回主题。要做到稳定控制,需要从约束输入和约束上下文两个层面同时下手:一方面用System Prompt把行为边界写清楚,另一方面用检索增强把回答依据锚定在相关材料上。

System Prompt约束的核心设计方法
System Prompt不是简单写几句角色描述,它承担着定义任务边界、排除干扰和锁定输出格式的职责。一个有效的System Prompt应该包含四个要素:角色与目标、可用信息范围、禁止行为清单、输出结构模板。角色与目标让模型明确自己在这个会话中的身份和要完成的具体任务,例如不是泛泛地说你是客服,而是说你是负责处理退款流程的客服,只回答退款相关问题,不回答产品咨询。可用信息范围明确告诉模型只能基于用户提供的材料或后续检索到的片段作答,不能引用训练数据中的记忆。禁止行为清单列出最常见的跑题方向,比如不要展开与当前问题无关的背景介绍,不要重复用户已经确认过的内容,不要主动追问不相关的细节。输出结构模板则直接给出期望的回答格式,比如要求先给出结论,再分点说明依据,最后给出操作步骤。
负面约束往往比正面约束更能减少跑题。很多开发者在System Prompt中只写了要做什么,却没有写不要做什么。模型在生成时会对所有可能的token计算概率,如果没有明确的排除项,与主题相关但偏离重点的内容依然会被采样出来。可以加入类似这样的语句:不要提及文档中没有出现过的产品名称;如果用户的问题超出了给定材料范围,直接回答无法确认,不要尝试补充额外信息。负面约束需要具体到行为,而不是抽象地说不要跑题。抽象指令模型理解不了边界,具体到禁止哪些动作才有效。
另外,在System Prompt中加入少量示例(few-shot)也能显著降低跑题率。示例最好选用容易跑题的边界案例,展示模型应该如何在信息不足或者问题模糊时主动收缩范围。比如给出一个示例:用户问这个产品好用吗,而材料中只有产品规格没有用户评价,模型应回答当前材料中未包含用户评价信息,无法判断好用程度。这种示例教会模型在不确定时选择克制,而不是自由发挥。
system_prompt = """ 你是退款流程助手。 你只能根据提供的订单信息和退款政策回答用户问题。 禁止事项: - 不要回答与退款无关的产品咨询 - 不要提及材料中没有出现的优惠活动 - 不要建议用户联系不存在的部门 输出格式: 1. 先给出结论(可以退款/不可以退款/需要人工审核) 2. 引用政策条款编号 3. 说明下一步操作 示例: 用户:这个手机屏幕碎了能退款吗? 材料:订单信息显示已签收15天,退款政策第2条:签收7天内可无理由退款,碎屏属于人为损坏不在退款范围。 回答:不可以退款。依据退款政策第2条,签收超过7天且碎屏为人为损坏,不在退款范围。建议联系维修服务。 """
这段System Prompt明确排除了产品咨询和无关优惠信息,并用一个边界示例展示了如何在材料不足时直接给出结论。实际工程中还可以把输出格式进一步锁定为JSON,让模型只能按照预设字段输出,极大压缩跑题空间。
检索增强如何把回答锚定在相关材料上
检索增强生成(RAG)的思路是,在模型生成回答之前,先从外部知识库中检索出与用户问题最相关的文本片段,把这些片段作为上下文注入到提示中。这样做的好处是,模型不需要依赖自己的参数记忆来回想可能已经过时或者不准确的信息,而是直接基于检索到的材料作答。跑题往往发生在模型对问题没有把握、开始用训练数据中的泛化知识填充时,RAG通过提供明确、有限的上下文,把模型的注意力重新聚焦到相关材料上。
检索的质量直接决定RAG效果。常用的流程包括:将知识库文档切分成较小片段,使用嵌入模型把片段和用户问题分别编码成向量,通过余弦相似度或其他距离度量召回最相似的Top K片段。但简单相似度召回会引入不少语义相近却与当前问题无关的片段,这些片段反而会加剧跑题。需要在召回后增加重排序步骤,使用交叉编码器或者更精细的语义匹配模型,根据片段与问题的真实相关性重新排序,只保留排名前几的结果。重排序能有效过滤掉那些表面相似但回答当前问题用不上的内容。
from sentence_transformers import SentenceTransformer, CrossEncoder
# 嵌入模型用于召回
embedder = SentenceTransformer('BAAI/bge-small-zh-v1.5')
# 交叉编码器用于重排序
reranker = CrossEncoder('BAAI/bge-reranker-base')
query = "退款申请被拒绝后如何申诉"
documents = [
"订单签收超过7天不支持无理由退款",
"退款申诉需要提供开箱视频和订单截图",
"换货流程需要联系在线客服提交工单",
"优惠券使用规则说明"
]
query_vec = embedder.encode(query)
doc_vecs = embedder.encode(documents)
similarities = [query_vec @ doc_vec / (len(query_vec)**0.5 * len(doc_vec)**0.5) for doc_vec in doc_vecs]
# 假设召回前3个
top_indices = sorted(range(len(similarities)), key=lambda i: similarities[i], reverse=True)[:3]
rerank_pairs = [[query, documents[i]] for i in top_indices]
rerank_scores = reranker.predict(rerank_pairs)
# 根据重排序分数只保留最相关的1个片段
best_idx = top_indices[int(rerank_scores.argmax())]
injected_context = documents[best_idx]
print(injected_context)
上述代码先是基于余弦相似度做了粗召回,然后使用交叉编码器对候选片段重新打分。粗召回的片段可能包含无关的换货和优惠券规则,但重排序后只有申诉相关片段被保留。把这段材料注入提示后,模型回答时就有明确的依据,跑题概率大幅下降。需要注意嵌入模型和重排序模型的选择要与业务语言匹配,中文场景优先使用针对中文优化的模型。
RAG还有一个容易忽略的细节:注入上下文的长度和位置。如果检索到的片段太长,会把关键指令推到上下文更远处,反而降低指令遵循效果。建议对片段做摘要或者截断,只保留与问题最直接相关的句子。注入时把检索材料放在用户问题之前,并用明确的标记分隔,例如使用特殊符号包裹材料,再在System Prompt中说明这些材料的优先级高于模型自身的知识。
System Prompt与RAG结合的工程实践
实际生产环境中,单独用System Prompt约束或者单独用RAG都可能不足以应对复杂的长对话和多轮交互。更稳妥的做法是把两者结合:System Prompt定义行为边界和输出格式,RAG提供当前问题所需的事实依据。每次用户提问时,先执行检索流程,将重排序后的相关片段注入到上下文,再连同严格的System Prompt一起发送给模型。这样模型既知道要回答什么问题,也知道只能基于哪些材料回答。
在工程实现上,建议维护一个上下文组装模块,统一管理System Prompt、历史消息、检索材料和用户当前输入。System Prompt在会话开始时固定,检索材料每轮动态更新。历史消息需要做截断或摘要,避免占用过多token稀释注意力。可以在每轮判断当前上下文中是否已经包含足够的相关材料,如果用户的新问题与上一轮检索结果高度重合,可以复用缓存,减少检索延迟。缓存键可以使用问题向量的哈希,命中后直接使用之前注入的材料,但要注意知识库更新时缓存失效问题。
评估跑题程度也需要专门的指标。可以构造一批带有明确预期答案范围的测试集,让模型输出回答后,用另一个模型判断回答是否聚焦在预期主题内,或者通过关键词覆盖率、实体一致性等统计方法自动打分。更直接的方式是人工抽检,记录跑题率变化趋势。上线后同时监控用户是否追问澄清、是否重新提问,这些行为往往是跑题的间接信号。结合System Prompt和RAG之后,如果跑题率下降不明显,需要检查两个方面:一是System Prompt中的负面约束是否足够具体,二是检索片段是否真的与当前问题强相关,有时候向量相似度最高的片段并不包含回答问题所需的关键信息,反而会把模型带偏。
# 上下文组装示例
def build_messages(system_prompt, user_query, retrieved_context, history_summary):
context_block = f"【检索材料】\n{retrieved_context}\n【材料结束】"
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"历史摘要:{history_summary}\n\n{context_block}\n\n当前问题:{user_query}"}
]
return messages
system_prompt = "你是退款助手,只依据检索材料回答。如果材料中没有相关信息,回答无法确认。不要补充材料以外的内容。"
retrieved_context = "退款申诉需要提供开箱视频和订单截图,审核周期为3个工作日。"
user_query = "申诉要哪些材料?"
history_summary = "用户之前咨询过退款被拒原因。"
messages = build_messages(system_prompt, user_query, retrieved_context, history_summary)
print(messages)
这段代码展示了上下文组装的基本结构,历史摘要和检索材料都被显式标记,System Prompt明确限定只能依据检索材料回答。实际部署时还需要处理材料过长时的截断策略,以及多轮对话中检索材料如何合并去重。如果检索材料包含多个片段,可以用编号列出,让模型在回答中引用来源编号,这样既能减少跑题,也方便后续追溯。
需要注意的是,System Prompt约束和RAG都不是银弹。当用户问题本身模糊或者知识库内容不完整时,模型仍然可能为了生成流畅的文本而跑偏。此时需要在System Prompt中加入兜底规则:如果无法确定用户的准确意图,主动追问澄清,而不是直接回答。追问本身也是一种防止跑题的手段,把模型的输出限制在提问范围内,避免它自作主张扩大话题。
模型回答跑题System Prompt检索增强修改时间:2026-10-05 02:51:10