ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Llama 2提示词工程实战:从对话模板到思维链的调优全攻略

Llama 2提示词工程实战:从对话模板到思维链的调优全攻略

我第一次接触这个模型时,最大的教训就是把别的闭源模型提示词原封不动地搬过来。结果模型要么回答得飘忽不定,要么频繁重复,要么完全无视我设定的输出格式。折腾一晚上之后才意识到:Llama 2提示词工程的第一道门槛,不是"你写了什么内容",而是"你根本没搞懂模型接受的对话结构"。这篇文章就是我调优这个模型以来沉淀下来的核心经验,从对话模板机制开始,到系统提示词、Few-shot、格式控制、思维链,再到各种翻车现场的复盘,最后给一套可以直接拿去用的提示词模板。适合刚把模型拉起来的开发者,也适合已经跑通但输出质量始终不满意的朋友。

1. 先把输入格式搞对:指令块机制的底层逻辑

1.1 Base版和Chat版是两种完全不同的物种

拿Llama 2来写提示词,第一件事就是要分清你手里的是哪个版本。模型官方发布了基座版和对话版两套权重,基座版本质上是文本续写模型,你给它一段文本它就往下续写,没有"指令跟随"的概念,提示词工程在它身上基本没有意义。真正能接收提示词指令的是对话微调版,它经过了特定格式的数据训练,所有对话能力都建立在一种特殊的输入模板之上。

我见过太多人下载了基座版然后抱怨"这个模型怎么不听话",本质上就是用错了对象。你拿对话版的要求去苛责基座版,就像让一个背过字典的人去写小说——他确实知道每个词怎么用,但完全不知道该怎么组织成一篇有逻辑的文章。所以做提示词工程之前,确认你自己加载的是Chat版,这是所有后续操作的前提。这个前提一旦错了,后面写得再漂亮都是白费。

1.2 指令块模板到底长什么样

对话版模型在训练时,输入格式固定为指令块结构。单轮对话的标准模板是这样的:

[INST] <<SYS>> 这里是系统提示词内容 <</SYS>> 这里是用户的第一轮输入内容 [/INST]

看到这个结构,你应该能理解为什么直接把其他模型的提示词搬运过来会失效。你的提示词内容没有放在指令块里,模型面对一堆裸文本,根本不知道该往哪个方向"接话"。

其中[INST]和[/INST]标记了一段用户指令的开始和结束,<<SYS>>和<</SYS>>用来框住系统提示词。系统提示词不是必填的,但如果你想用它,必须放在指令块的起始位置。实际使用中还有一个容易出问题的点:<</SYS>>后面要空一行,再接用户输入;用户输入之后、[/INST]之前要有一个空格。这些细节看着不起眼,但模型在训练时看到的格式就是这么严苛,推理时你给的格式稍有出入,模型的响应质量就会波动。

如果你是用某个主流深度学习框架的transformers接口加载模型,可以用一个现成的对话模板方法来自动拼装,省得自己手工拼接字符串。但你要明白这些框架做的只是"把消息列表翻译成指令块格式",底层的每一处换行和标签仍然有意义。

1.3 多轮对话的拼接方式是很多人忽略的点

多轮对话比单轮复杂得多。模型训练的格式要求:每一轮对话都要依次堆叠在一起,助手的历史回答要原样保留在上下文里,并且紧跟在对应的用户指令块后面。完整的两轮对话模板长这样:

[INST] <<SYS>> 你是一个严谨的助手 <</SYS>> 第一轮用户提问 [/INST] 第一轮助手回答 [INST] 第二轮用户提问 [/INST]

注意几个关键点:上一轮的助手回答和下一轮的[INST]之间是空格分隔,而不是换行单独隔开;整段输入是一个连续拼接的序列,中间不能多加多余的标签;如果你用transformers库的多轮消息列表,框架会自动帮你拼接,但只要你开始手写模板,就必须严格按这个顺序来。

我在实际项目里踩过这个坑:手工拼多轮模板时漏掉了历史助手回答,结果第二轮的回答质量骤降,模型经常"失忆",甚至会开始胡编乱造。原因很直观——模型在训练时通过历史回答来区分"对话进行到哪一轮"以及"我应该延续什么角色",你把这个信息剪掉了,它自然丢失了对整个对话的把握。

2. 基础提示词三件套:系统词、角色设定、Few-shot

2.1 系统提示词:传空字符串和完全不传是两码事

很多人以为系统提示词可有可无,反正模型都能回答问题。这个认知在Llama 2上有偏差。官方对话模板里,系统提示词的位置是独立存在的,即使你不需要任何系统约束,也建议显式传入一个空字符串,让模板保持完整结构。这个细节来自模型训练时的数据分布:训练样本中大量对话即使系统内容为空,也依然保留了<<SYS>>和<</SYS>>的空标签结构。推理时不传这个结构,模型在面对指令时的"姿态"会略有不同,反应在输出上就是略微降低指令遵循的稳定性。

更实际的做法是:尽量把系统提示词利用起来。我的通用模板是"身份定义 + 行为规则 + 输出格式约定",三者按顺序放在一段话里。系统提示词在每一轮对话都会生效,适合放那些你希望模型始终遵守的高层级约束。比如你希望模型始终用简体中文回答、始终不泄露内部推理过程、始终以列表形式输出,这些都可以写进系统提示词。

2.2 角色设定的颗粒度决定了模型的"表演上限"

"你是一个有用的助手"这种角色设定效果很有限。模型确实会进入一个角色,但角色越模糊,模型对输出的塑造自由度就越大。我在实践中比较有效的是把角色设定描述到"可执行的输出层",明确这个角色的工作方式、输出偏好和禁止动作。

对比就很直观:

角色设定方式效果
你是一个助手输出泛泛,语气不稳定,格式时好时坏
你是一个严谨的技术写作助手,擅长把复杂概念讲清楚,输出使用简洁段落,禁止使用列表之外的冗余描述输出稳定,风格一致,格式基本符合预期

原因在于:角色设定越具体,模型在解码时可供选取的"输出风格空间"就越窄,每次生成的结果自然更收敛。如果你希望模型偶尔有创造性,角色就松一点;如果你希望它稳定输出结构化的内容,角色就紧一点。这个颗粒度是可以根据任务性质调整的,不必所有场景都用同一套模板。

2.3 Few-shot示范:示范格式比示范内容更重要

Few-shot在Llama 2上的作用是真实存在的,但这个模型对示范的"格式"比对示范的"内容"更加敏感。换句话说,你给两个示例,模型学到的主要是"原来输出应该是这个结构",而不是"原来答案应该长这样"。所以设计示例时要刻意把你的目标输出格式放进示例里,而不仅仅是提供一段标准答案。

举个例子,如果你希望模型输出"结论 + 理由列表 + 补充说明"三段式结构,那就一定要给一个严格遵守这个结构的示例,哪怕是虚构的场景都行。模型看到示例的结构,就会倾向复制这个结构去组织它的回答。这与人类学习很像,你给一个学生展示标准答卷的排版和给一个学生展示标准答卷的内容,前者学会的是答题框架,后者学到的是知识本身,框架的泛化能力远比内容强。

另外,2到3个示例在多数场景下已经够用。示例太多反而会让模型过度模仿示例的措辞细节,导致照搬句式而不是理解任务本身。我通常在示例之间标注了输入和输出,并且示例风格与真实任务的复杂度保持一致。复杂度差距过大会让模型犯迷糊,它可能学会了示例的框架,却被示例里没出现过的任务难度打乱了阵脚。

3. 让模型稳定输出格式的三类硬手段

3.1 JSON输出的拆弹式写法

用过闭源模型API的人,会有一种"只要在提示词里写'输出JSON',模型就会乖乖给JSON"的错觉。放到Llama 2上,这个错觉很快就会被现实打碎。模型对"JSON"的理解远不如闭源模型那么扎实,经常出现字段名被改动、多余解释文字、Markdown代码块包裹等幺蛾子。指望一遍就得到合法JSON,风险偏高。

我常用的提示词写法和解析逻辑是这样配合的。

先是提示词部分,明确字段结构加禁语:

请从以下文本中提取信息,输出JSON格式,必须包含字段:title、author、year、summary。 只输出JSON对象本身,不要输出任何解释或Markdown代码块标记。\n文本:{这里放置目标文本}

然后是解析部分,高容错处理:

import json, re def parse_llama_json(raw: str): # 剥离可能存在的Markdown代码块包裹 cleaned = raw.strip() cleaned = re.sub(r"^```(?:json)?|```$", "", cleaned, flags=re.MULTILINE).strip() try: return json.loads(cleaned) except json.JSONDecodeError: # 常见容错:提取第一个{到最后一个}之间的内容 start = cleaned.find("{") end = cleaned.rfind("}") if start != -1 and end != -1 and end > start: return json.loads(cleaned[start:end+1]) raise

这段代码虽然简单,但解决了80%以上的脏输出问题。我的经验是,提示词那边的"不要输出任何解释或Markdown代码块标记"不能省,少了这句,模型很容易在JSON前后垫两句多余的说明。

还有一个隐蔽问题是字段名被模型加工,例如summary被改成Summary或者summarize。要让字段名稳定,最有效的办法是在系统提示词或者Few-shot示例里展示一次"标准字段名对照样例",让模型把这个清单当成字典抄写。语义理解能力再强的模型,在字段命名上依然有可能自由发挥,你得给它一个抄作业的模板。

3.2 输出风格的负面约束组合拳

我发现这个模型在"避免出现某种风格"时,负面约束的表述方式直接影响效果。笼统地说"不要使用人工智能腔调"基本没用,模型不理解什么叫人工智能腔调。更有效的是把"不要做"翻译成"不要使用的具体词汇和句式"。

我常用的一段约束是这样写的:

避免使用以下词汇和表述:首先、其次、最后、总的来说、此外、值得注意的是、随着技术的发展。不要用排比句,不要用总结性段落,保持直接、朴素、口语化的中文。

用这种"负面清单"式约束,模型对禁用特征的捕捉明显更准确。原因是这个模型对具体token和具体句式的判别能力强,但抽象概念理解能力弱。"AI腔调"是个抽象概念,模型无法把它映射到具体的token序列上;但"避免使用'首先''其次''最后'"是具体的token级约束,模型在解码时确实会抑制这些词的出现。

组合使用的时候效果更好:给它一个正面示范示例展示"希望成为的样子",同时给它一个负面禁用清单展示"绝对不能出现的样子"。一正一反两个约束把模型的输出空间压得很窄,生成结果的可控性显著提高。

3.3 采样参数是提示词的外部调节旋钮

提示词不是唯一可以调节的因素。经常有人问我,为什么同样的提示词,自己机器上的输出忽好忽坏,而对方机器的输出一直很稳定。多数情况是解码参数不一致。

我自己在调这个模型时形成了一套固定的参数套路。结构化输出任务温度直接拉到0.1到0.3,这个区间模型输出最保守,格式失误率最低;创意写作温度会放到0.7到0.9,保留一定的多样性,但同时会加高重复惩罚来抑制复读。top_p一直保持在0.9附近,再高的话输出就容易跳脱,再低的话显得死板。模型的重复惩罚因子建议在1.1到1.3之间,这个值太小吃亏在长文本丢失细节,太大会出现语义跳跃甚至自相矛盾。

温度和所有采样参数的本质是改变解码时的概率分布。提示词告诉你"模型应该说什么",参数告诉你"模型有多敢偏离最可能的选择"。两者是配合关系:提示词约束好方向和格式,参数控制犒赏范围和稳定性。只调提示词不调参数,就像车头摆正了但方向盘没回正,还是会跑偏。

4. 复杂任务怎么提示:思维链与任务拆解

4.1 思维链的两种写法,效果差很多

处理数学推理、逻辑推断、多条件判断这类任务时,直接要答案大概率会错。这个模型本身就是自回归预测下一个token,跳过推理步骤直接答结果,很容易在中间某个环节偷懒。思维链提示的核心很朴素:让模型把推理步骤写到明面上,一步一步推导,每步都在明确的输入输出中计算,减少跳步造成的幻觉。

写法一是我直接要求它"请一步一步地思考,并展示你的推理过程",这个模型能理解,但在复杂场景下容易把推理过程写得过于冗长,而且最终答案藏在长篇推理里,不方便后续程序提取。我更推荐的写法是同时要求它"先输出推理过程,再另起一行输出最终答案"。把推理过程和结论分离开,模型在结构上会更清晰,你也能在解析时直接取到最后一行。

写法二更高级一些,是让模型"自问自答"。写法如下:

请对以下问题进行逐步推理。你必须先列出关键疑点,再针对每个疑点回答,最后基于这些回答给出结论。

这种写法的优势在于强制模型先拆解问题的维度,再逐个击破。实际效果上,对于多条件判断类的问题,这种方式比单纯说"一步步思考"的错误率低不少。我猜测原因是"列出疑点"这个动作本身迫使模型建立了一个覆盖所有信息的清单,防止它凭直觉略过某个关键条件。

无论用哪种写法,有一点要时刻记住:思维链提示会显著增加输出token数,同时意味着推理时间变长。在CPU上跑或者用低推理能力的量化版本时,长思维链可能让等待时间成倍增长。这时候要评估任务复杂度,简单任务没必要上思维链,杀鸡用牛刀反而拖慢整体链路。

4.2 子任务拆解:模型上下文里的一次性工程

对于特别复杂的任务,思维链也扛不住,需要在提示词层面做三层拆解。比如让模型根据一份原始数据写一份四段式的分析报告。直接在一条提示词里给完整数据并提出全部要求,输出质量通常达不到预期。模型面对的信息和约束太多,"想要都满足"和"注意力有限"碰撞,结果往往是形式到位但内容空洞。

更好的做法是分步骤进行。第一步只让它梳理数据里的关键指标,输出结构化摘要;第二步再让它根据摘要拟定报告大纲;第三步才让它把大纲扩写成完整报告。每一步都是独立的提示词,上一步输出作为下一步输入的一部分。这种做法能大幅提升内容质量,因为每一段推理都在一个精力聚焦的小范围内进行。

这里有个很关键的设计原则:不要让模型一次性处理它处理不了太多信息。这个模型在生成长文本时,会逐渐丢失对早期内容的忠实度,输出越长,后文越容易与前面要求脱节。分步拆解天然让每一步的输出规模可控,每一段内容都保持在对上下文注意力最集中的长度区间内。

4.3 上下文窗口耗尽:从提示词层面预防

这个模型的上下文窗口是有限资源。长对话跑着跑着,最早的对话内容会被挤出去。很多人以为这事只能靠换更大上下文的模型解决,其实提示词层面也能做预防。

我的做法是给对话设置一个"窗口预算"。假设上下文窗口长度为4096,估算每次回答平均消耗的token数,推算出对话能在多少轮之内安全运行。超过就触发一轮摘要压缩:让模型把到目前为止的对话总结成一个简短的摘要,然后抛弃原始对话,用"摘要 + 新问题"开启新一轮对话。这样虽然损失了一部分细节,但模型不会因为上下文截断而精神错乱。

另外一个容易忽视的点是,历史消息里那些已经落地的指令约束其实不需要重复保留。系统提示词是每一轮都要重新加载的,所以所有长期有效的约束都应该放在系统提示词里,而不是随着用户消息反复传。用户消息部分只保留对话本身,这样能省下一大截上下文空间用来放真正有价值的信息。

5. 我踩过的五个典型提示词坑

5.1 失败案例复盘

调这个模型的过程中,我把各种类型的翻车都经历过一遍。有些坑事后看全是基础问题,但当时确实花了不少时间排查。整理成表格方便你对比自查。

现象根因对应解法
模型对要求无动于衷,回答泛泛提示词没有包进指令块,或者加载了基座版严格走对话模板,确认Chat版权重
输出格式时好时坏,偶尔连续重复解码参数没有固定,温度过高结构化任务温度降到0.1-0.3,开重复惩罚
对话多轮之后突然"失忆"手工拼接模板漏掉了历史助手回答完整保留每一轮[INST]...[/INST]链
要求输出JSON,结果字段乱变没有给字段名对照样例,模型自由发挥用Few-shot展示一次目标JSON结构
提示词越长输出越散,重点全丢所有约束堆在一条提示词里,注意力被稀释拆成多轮子任务,每轮聚焦一个目标

"提示词太长导致注意力稀释"这个坑特别隐蔽。你可能觉得约束给得越细,模型越知道你想要什么。但实际是这个模型对长提示词中的每一条约束都有一定程度的趋同处理,约束条目太多时,次要约束会开始破坏主要约束的表达效果。处理办法就是拆分任务,而不是延长提示词。

5.2 一套可以直接复用的综合模板

聊了这么多理论和坑,最后分享一个我目前在多数场景下直接复用的模板。这个模板整合了前面所有原则:系统提示带角色和约束,用户消息明确输出格式,必要时加一两个Few-shot示例。

[INST] <<SYS>> 你是一个严谨的工程文档助手。你的任务是将用户提供的原始描述整理为结构化内容。 输出必须满足以下约束: 1. 使用简体中文。 2. 使用二级标题和三级标题组织内容,标题要求直接、具体。 3. 所有结论必须来源于给定描述,不得补充虚拟信息。 4. 禁止使用"首先""其次""综上所述"等套话词汇。 <</SYS>> 请将以下原始描述整理成结构清晰的文档。输出格式为Markdown,直接开始输出正文,不要输出任何额外说明。 原始描述:{这里放置用户内容} [/INST]

这套模板的要点在于:系统提示词把"你用什么样的身份、输出什么风格、禁止什么行为"全部锁死;用户消息部分把任务目标和输入内容清晰切分;模板格式严格遵循指令块结构。我实际用下来的感受是,在多数内容整理类任务中它都能一次达到可用水平,不需要再做二次润色。

5.3 参数和提示词的最后一层配合

最后补一个容易被忽略的参数:输出长度限制。给模型设定一个max_new_tokens上限,在提示词层面其实也有讲究。如果你在系统提示词里写了"请用不超过300字回答",这个约束对模型来说往往不够硬。模型生成过程中没有真正的"数数字"能力,它只是在生成时感觉差不多就停。配合参数设置严格上限,同时提示词用"用简短的三个要点回答",这种"结构限制 + 长度限制"的组合会比单纯说"不超过300字"可靠得多。

这可能也是很多提示词指南里没有人明说的地方:提示词和参数是协同调优的关系,两者要一起调,不能只指望一边。提示词给模型指一个大概的方向,参数负责把生成结果锁在收敛的范围内,两者配合得好,这个开源模型的表现力并不逊色于闭源大模型的日常使用体验。我在项目里把这段调优经验沉淀成了一套固定配置,每次接新任务先套模板,再根据任务类型微调参数和示例,整体效率比一开始"自由发挥"式的瞎试高了很多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进