3.3 System Prompt 设计:构建角色与约束
承前
在上一节中,我们学习了高级 Prompt 策略——从少样本提示到思维链推理,再到 ReAct 模式的"思考-行动-观察"循环。这些策略教会我们如何让模型在单次交互中表现得更加聪明、更加可控。然而,它们都聚焦于一个层面:如何把"这一轮"的用户请求写得更好。
但在真实的生产环境中,我们面对的不是一个孤立的问题,而是一场持续的对话:用户可能连续提问十几轮,中间切换话题,甚至给出相互矛盾的指令。如果每一轮都靠用户自己来约束模型的行为,那体验将非常脆弱。我们需要一个更底层的机制——在对话开始之前就为模型设定好"它是谁、能做什么、不能做什么"。
这就是 System Prompt 的职责。它不是某一轮对话的"提问技巧",而是整个交互过程的"行为框架"。本节将从分工原则、角色设计、行为约束和动态生成四个维度,系统讲解如何构建一个生产级的 System Prompt。
一、System Prompt vs User Prompt:分工之道
现代 LLM 的消息结构通常包含三个角色:
┌─────────────────────────────────────────────────────────────┐
│ Chat Completion 消息结构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ System Message (系统消息) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 定义"你是谁"和"如何行动" │ │
│ │ • 角色身份、专业领域 │ │
│ │ • 行为准则、安全边界 │ │
│ │ • 输出格式、语气风格 │ │
│ │ • 持久生效,贯穿整个对话 │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ User Message (用户消息) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 定义"做什么"和"给什么" │ │
│ │ • 具体任务/问题 │ │
│ │ • 输入数据和上下文 │ │
│ │ • 临时指令(覆盖或补充System) │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ Assistant Message (助手消息) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 模型的历史回复,构成对话上下文 │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘理解分工的一个好类比:给新员工的岗位说明书。
想象你正在招聘一名新员工。在他入职的第一天,你不会只说"去干活吧"——那样他会完全不知道自己该做什么。你会先给他一份岗位说明书,上面写着:你的职位是什么、负责哪些业务、能做哪些决策、不能碰哪些东西、汇报对象是谁、用什么样的格式写周报。这份说明书不是针对某一天某一项具体工作的,而是对他整个任期的行为框架。
System Prompt 就是这份岗位说明书。它定义了模型在整个对话中的身份、能力和边界。而 User Prompt 则是每天具体派发的工作任务——"帮我审查这段代码"、"翻译这个文档"、"总结这篇文章"。员工可以根据岗位说明书的精神,自主处理各种日常任务;遇到超出职责范围的事,他会主动说"这不在我的负责范围内"。
这个类比揭示了一个核心原则:
| 维度 | System Prompt | User Prompt |
|---|---|---|
| 作用域 | 全局、持久 | 单次、临时 |
| 内容 | 角色、规则、约束 | 任务、数据、问题 |
| 修改频率 | 低(应用级配置) | 高(每次交互) |
| 类比 | 岗位说明书 | 每日工作派发 |
理解了这一点,我们就知道为什么很多初学者的 Prompt 效果不稳定了——他们把角色定义、行为规则全塞进了 User Prompt,相当于每天重新给员工交代一遍岗位职责,既冗余又容易遗漏。
下面是一个完整的代码示例,展示了 System Prompt 和 User Prompt 如何协同工作:
# System Prompt:定义"助手是什么"
# 这段文本只在对话开始时发送一次,之后每一轮对话都自动携带
system_prompt = """你是一个专业的代码审查助手。
- 你精通 Python、JavaScript、Go 三种语言 # 限定专业领域,避免模型什么都答
- 你关注代码安全、性能和可维护性 # 指明审查重点,让输出有方向
- 你的回答简洁、直接,以代码示例为主 # 规定输出风格,避免冗长
- 如果不确定,你会诚实说明""" # 设定诚实底线,防止幻觉
# User Prompt:每次交互的具体任务
# 每一轮对话的内容都不同,但模型始终遵循上面的角色设定
user_prompt = """请审查以下 Python 代码的安全性:
def process_user_input(data):
query = f"SELECT * FROM users WHERE name = '{data}'"
return db.execute(query)"""
# 组装 API 调用
# messages 是一个列表,第一个元素通常是 system,后续是交替的 user/assistant
messages = [
{"role": "system", "content": system_prompt}, # 系统消息:全局行为框架
{"role": "user", "content": user_prompt} # 用户消息:本次具体任务
]
# 调用 API 后,模型会同时参考 system 和 user 两层信息来生成回复
# system 决定"怎么答"(角色、风格、约束),user 决定"答什么"(具体内容)注意最后一行注释:模型不是简单地"先看 system 再看 user",而是将两者融合理解。当 User Prompt 中的临时指令与 System Prompt 的规则冲突时,模型通常会优先遵循 System Prompt——这正是我们在生产环境中依赖的行为:即使用户试图绕过规则,System Prompt 中设定的安全约束依然生效。
二、角色设计:从模糊到精准
好的角色设计不是简单地写一句"你是一个 XX 助手"就万事大吉。这就好比岗位说明书上只写了"职位:工程师"三个字,新员工依然不知道自己该做什么。真正有效的角色定义需要从多个维度精确定义模型行为。
角色设计五维模型:
┌─────────────────────────────────────────────────────────────┐
│ 角色设计五维模型 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ① 身份 (Identity) │
│ 你是谁?你的专业背景是什么? │
│ 例: "你是一位拥有10年经验的资深Python后端工程师" │
│ │
│ ② 能力 (Capabilities) │
│ 你能做什么?你的知识边界在哪里? │
│ 例: "你精通Django、FastAPI,了解数据库优化和系统设计" │
│ │
│ ③ 行为 (Behavior) │
│ 你如何与用户交互?你的工作方式是什么? │
│ 例: "你在给出代码前先分析问题,不确定时会主动提问" │
│ │
│ ④ 约束 (Constraints) │
│ 你不能做什么?你的安全边界是什么? │
│ 例: "你不生成恶意代码,不泄露用户隐私,不越权操作" │
│ │
│ ⑤ 输出 (Output Format) │
│ 你的回答应该是什么格式? │
│ 例: "代码用```python标记,分析用TODO/问题/建议三段式" │
│ │
└─────────────────────────────────────────────────────────────┘我们逐个维度来理解:
- 身份回答的是"你是谁"。不是笼统地说"你是助手",而是给出具体的专业背景。"你是一位拥有 10 年经验的 Python 后端工程师"比"你是程序员"有效得多,因为它让模型调用更精准的领域知识。
- 能力划定的是"你能做什么"。明确能力边界,既避免模型过度承诺,也帮助它在能力范围内给出更专业的回答。注意,"你能做什么"同样隐含了"你不能做什么"——没列出的领域,模型应当谨慎回答。
- 行为定义的是"你怎么做"。同样一个问题,"先分析再给代码"和"直接给代码"是两种截然不同的交互方式。行为准则让模型的输出风格可预期。
- 约束划定的是"你不能做什么"。这是生产环境中最关键的部分——安全边界、权限边界、道德边界都在这里定义。后文会专门展开。
- 输出规定的是"你的回答长什么样"。是否用 Markdown?是否分点?代码块用什么语言标记?这些格式约定让输出可以被下游程序可靠解析。
下面是一个完整的角色设计示例——技术面试官:
你是一位资深技术面试官,专门负责后端开发的面试。
【身份】
- 你在FAANG公司有8年后端开发和面试经验
- 你面试过超过500位候选人,从初级到资深级别
【能力】
- 你精通数据结构、算法、系统设计、数据库
- 你能评估候选人的代码质量、问题解决能力和沟通技巧
- 你的面试风格:先给简单问题热身,逐步加深难度
【行为准则】
- 你以友好、鼓励的态度开始,缓解候选人紧张
- 当候选人卡住时,你给出提示而非直接给答案
- 你关注候选人的思考过程,而非仅仅看最终结果
- 面试结束时你给出建设性反馈
【面试流程】
1. 自我介绍和破冰(1-2分钟)
2. 一道简单算法题热身(5-8分钟)
3. 一道中等难度系统设计题(10-15分钟)
4. 候选人提问环节(3-5分钟)
【约束】
- 不要一次性问多个问题
- 不要使用过于冷门或偏门的知识点
- 保持专业,不涉及面试无关话题这个示例完整覆盖了五个维度。值得注意的是【面试流程】这一段——它不是单纯的"行为准则",而是把行为准则具象化为一个可执行的标准流程。这种"准则 + 流程"的组合方式,能让模型的行为更加稳定可控。
三、行为约束与安全规则
在生产环境中,行为约束是 System Prompt 最关键的部分。回到我们的岗位说明书类比:新员工不仅需要知道"做什么",更需要清楚"不能做什么"——不能泄露客户数据、不能擅自批准超过权限的预算、不能在没有授权的情况下代表公司对外发声。一个缺失约束的 AI 就像一个没有权限边界的员工,可能产生严重后果。
约束层次结构:
┌──────────────────────────────────────────────────┐
│ 约束层次金字塔 │
├──────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ │
│ │ 法律合规 │ ← 最高优先级 │
│ │ (不可违反) │ │
│ └──────┬───────┘ │
│ │ │
│ ┌──────▼───────┐ │
│ │ 安全约束 │ ← 防止危害 │
│ │ (严格执行) │ │
│ └──────┬───────┘ │
│ │ │
│ ┌──────▼───────┐ │
│ │ 业务规则 │ ← 领域特定 │
│ │ (按需配置) │ │
│ └──────┬───────┘ │
│ │ │
│ ┌──────▼───────┐ │
│ │ 输出规范 │ ← 格式要求 │
│ │ (灵活调整) │ │
│ └──────────────┘ │
│ │
└──────────────────────────────────────────────────┘这个金字塔从上到下,优先级递减。法律合规是不可逾越的底线——无论用户如何要求,模型都不能协助违法活动。安全约束紧随其后——不生成有害内容、不泄露敏感信息。业务规则是领域特定的——比如电商客服不能承诺超出退换货政策的退款。输出规范最灵活——格式要求可以根据实际需要调整。
理解了层次结构,我们来看一个完整的安全约束示例:
【安全约束 - 必须遵守】
1. 内容安全
- 不生成任何涉及色情、暴力、歧视的内容
- 不提供制造武器、毒品、恶意软件的方法
- 不协助任何形式的欺诈或违法行为
2. 隐私保护
- 不要求用户提供密码、身份证号、银行卡号等敏感信息
- 不在回答中保留或展示用户的个人信息
- 当用户无意中暴露隐私时,提醒其注意安全
3. 权限边界
- 不执行超出授权范围的操作
- 不访问、修改或删除用户未明确授权的文件
- 不绕过任何安全限制或访问控制
4. 透明性
- 当不确定答案时,明确告知用户
- 当无法完成任务时,解释原因并提供替代方案
- 明确区分"事实"和"推测"约束的优先级处理也是设计中的关键环节。当多个约束发生冲突时,如果没有明确的优先级规则,模型的行为就会变得不可预测。例如,用户可能说"请尽可能详细地回答"(输出规范层面),但同时安全约束要求"不泄露敏感信息"。这时安全约束必须优先:
约束优先级(从高到低):
1. 安全约束 > 业务规则 > 输出规范
2. 用户明确指令 > 默认行为规则
3. 法律合规 > 用户体验注意第 2 条似乎与我们的直觉相反——用户明确指令的优先级高于默认行为规则。但这有一个前提:用户指令不能违反更高层的安全约束。换句话说,只有在不触及安全底线的前提下,用户指令才优先于默认规则。
四、动态 System Prompt
到目前为止,我们讨论的 System Prompt 都是固定文本——写好后就不变了。但在实际业务中,同一套固定 Prompt 很难满足所有场景。还是用岗位说明书的类比:同一个岗位在不同项目阶段需要不同的侧重,资深员工和新人的权限边界也不同。我们需要的是一份"可参数化"的岗位说明书。
动态 System Prompt 的概念:根据对话上下文、用户画像、业务状态等变量,在运行时动态生成 System Prompt,而非使用固定文本。
为什么需要动态 System Prompt:
- 个性化:不同用户需要不同的助手行为。VIP 客户需要更详细的分析,普通客户需要简洁的答案。
- 上下文感知:根据对话阶段调整角色。开场时热情引导,深入问题时切换为分析模式,结束时转为总结确认。
- A/B 测试:对比不同版本 Prompt 的效果。同一业务场景,分别用两套 Prompt 跑一周,看哪套的转化率更高。
- 多租户:同一个模型服务多个客户,每个客户有不同的业务规则。A 公司的客服不能提 B 公司的产品信息。
动态 System Prompt 架构:
┌─────────────────────────────────────────────────────────────┐
│ 动态 System Prompt 生成流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ 基础模板 │ │ 用户画像 │ │ 上下文变量 │ │
│ │ (Jinja2) │ │ (权限等) │ │ (对话轮次/状态) │ │
│ └────┬─────┘ └────┬─────┘ └────────┬─────────┘ │
│ │ │ │ │
│ └───────────────┼───────────────────┘ │
│ │ │
│ ┌────▼─────┐ │
│ │ 渲染引擎 │ │
│ └────┬─────┘ │
│ │ │
│ ┌────▼─────┐ │
│ │ System │ │
│ │ Prompt │ │
│ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘三个输入源汇入渲染引擎,最终产出定制化的 System Prompt。下面我们来看具体实现:
from jinja2 import Template
# 基础 System Prompt 模板
# 用 Jinja2 模板语法,{{ }} 是变量占位符,{% %} 是逻辑控制语句
BASE_SYSTEM_TEMPLATE = """
你是一个{{ role }},专门服务于{{ company }}的{{ department }}部门。
【当前用户信息】
- 用户级别:{{ user_level }}
- 使用场景:{{ scenario }}
【核心能力】
{{ capabilities }}
【行为准则】
{{ behavior_rules }}
【当前上下文】
- 对话轮次:第{{ turn_count }}轮
- 当前时间:{{ current_time }}
- 用户情绪:{{ user_mood }}
{% if user_level == 'vip' %} # 条件渲染:仅 VIP 用户看到这段
【VIP专属服务】
- 提供更详细的分析和更多示例
- 优先处理复杂请求
{% endif %}
{% if scenario == 'debug' %} # 条件渲染:仅调试场景看到这段
【调试模式】
- 输出中间推理步骤
- 显示所有工具调用详情
- 标注信息置信度
{% endif %}
"""
# 根据用户和上下文动态渲染 System Prompt
def generate_system_prompt(user_profile, context):
"""根据用户画像和上下文信息,动态生成 System Prompt
Args:
user_profile: 用户画像字典,包含角色偏好、公司、部门等信息
context: 上下文字典,包含当前对话轮次、时间、用户情绪等
Returns:
渲染后的 System Prompt 字符串
"""
template = Template(BASE_SYSTEM_TEMPLATE) # 加载 Jinja2 模板
return template.render( # 用实际数据填充模板变量
role=user_profile.get("preferred_role", "AI助手"), # 默认值"AI助手"
company=user_profile.get("company", "默认"), # 默认公司名
department=user_profile.get("department", "技术"), # 默认部门
user_level=user_profile.get("level", "standard"), # 默认普通用户
scenario=user_profile.get("scenario", "general"), # 默认通用场景
capabilities=user_profile.get("capabilities", "通用问答"),
behavior_rules=user_profile.get("rules", "友好专业"),
turn_count=context.get("turn", 1), # 默认第1轮对话
current_time=context.get("time", ""), # 当前时间
user_mood=context.get("mood", "neutral") # 用户情绪默认中性
)这段代码的关键设计在于:模板定义了结构,数据填充了细节。当用户是 VIP 时,模板中 {% if user_level == 'vip' %} 分支会自动展开,注入专属服务指引;当场景是调试模式时,调试相关的指令会自动加入。同一个模板,不同的输入数据,产出不同侧重的 System Prompt。
动态 Prompt 的常见模式:
| 模式 | 说明 | 示例 |
|---|---|---|
| 条件渲染 | 根据条件决定是否包含某段内容 | VIP用户看详细指引,普通用户看精简版 |
| 变量注入 | 将用户/上下文数据嵌入 Prompt | 用户名、偏好语言、历史记录 |
| 模板继承 | 基础模板 + 场景覆盖 | 客服场景覆盖通用助手模板 |
| 规则引擎 | 根据业务规则动态组合 Prompt 片段 | 根据产品类型加载不同的FAQ |
这四种模式可以组合使用。比如一个多租户客服系统,可能同时用到模板继承(每个租户继承基础客服模板)、变量注入(注入租户名称和产品列表)、条件渲染(根据用户是否为 VIP 决定服务等级)和规则引擎(根据用户咨询的产品类型动态加载对应的 FAQ 片段)。
五、实战练习
练习1:设计客服助手的 System Prompt
目标:为一家电商平台设计一个客服 AI 助手的 System Prompt。
要求:
- 角色:友善、耐心的客服专员
- 能力:处理退换货、订单查询、物流追踪
- 行为:先安抚情绪,再解决问题
- 约束:不承诺无法兑现的退款、不泄露用户隐私
- 输出:结构化、有步骤指引
参考模板:
你是一家名为"云购商城"的电商平台的AI客服助手,名字叫"小云"。
【身份】
- 你是云购商城的专属客服,代表平台形象
- 你的风格:温暖、耐心、专业
【能力范围】
你可以帮助用户处理以下事务:
1. 订单查询:通过订单号查询订单状态
2. 物流追踪:查询包裹实时位置和预计送达时间
3. 退换货申请:引导用户完成退换货流程
4. 常见问题:商品信息、支付方式、优惠活动等
【服务流程】
面对用户问题时:
1. 先表达理解和共情("我理解您的感受...")
2. 确认问题细节(订单号、时间等)
3. 给出明确的解决方案和步骤
4. 询问是否还有其他需要帮助的
【约束】
- 不承诺平台政策之外的退款或补偿
- 不索要用户密码、银行卡号等敏感信息
- 遇到无法处理的问题,引导用户转接人工客服
- 不与其他平台进行比较或贬低
【输出格式】
你的每次回复应包含:
😊 共情回应
📋 问题确认
✅ 解决方案(分步骤)
❓ 后续跟进完成后,用以下测试场景验证:
测试场景1:用户说"我买的手机三天了还没发货,我要退款!"
测试场景2:用户说"物流显示已签收但我没收到,怎么回事?"
测试场景3:用户说"你们的客服电话是多少?我想投诉刚才那个客服"这三个场景分别测试了情绪安抚能力、异常处理能力和边界感知能力。如果你的 System Prompt 设计得当,模型应该在每个场景中都遵循"先共情、再确认、后解决"的流程,同时在无法处理时主动转接人工客服。
练习2:实现动态 System Prompt 生成器
目标:编写一个 Python 函数,根据用户身份和对话阶段动态生成 System Prompt。
from jinja2 import Template
from datetime import datetime
# 动态模板:角色和时间会根据输入变化
SYSTEM_TEMPLATE = """
你是一个{{ assistant_type }}。
【当前时间】{{ current_time }}
【用户身份】{{ user_type }}
【对话阶段】{{ stage }}
{{ stage_instructions }}
"""
def get_stage_instructions(stage):
"""根据对话阶段返回不同的行为指令
对话分为四个阶段,每个阶段对助手的行为要求不同:
- greeting: 开场破冰,态度要热情
- problem: 收集信息,要善于追问
- solution: 给出方案,要条理清晰
- closing: 确认满意度,邀请评价
"""
stages = {
"greeting": "请友好地欢迎用户,并询问今天需要什么帮助。",
"problem": "请认真听取用户问题,通过追问确认细节。",
"solution": "请提供明确的解决方案,分步骤说明。",
"closing": "请确认问题是否解决,并邀请用户评价服务。"
}
return stages.get(stage, "请提供专业帮助。") # 未知阶段用默认指令
def generate_dynamic_prompt(user_type, stage):
"""生成动态 System Prompt
Args:
user_type: 用户类型,"customer"为外部客户,其他为内部员工
stage: 对话阶段,见 get_stage_instructions 的说明
Returns:
渲染后的 System Prompt 字符串
"""
template = Template(SYSTEM_TEMPLATE) # 加载 Jinja2 模板
return template.render( # 填充变量
assistant_type="客服助手" if user_type == "customer" else "内部技术支持",
current_time=datetime.now().strftime("%Y-%m-%d %H:%M"), # 实时时间
user_type=user_type,
stage=stage,
stage_instructions=get_stage_instructions(stage)
)
# 测试:同一模板,不同输入,产出不同的 System Prompt
print(generate_dynamic_prompt("customer", "problem")) # 外部客户 + 问题阶段
print("---")
print(generate_dynamic_prompt("internal", "solution")) # 内部员工 + 方案阶段挑战:扩展这个框架,增加对话历史感知和用户情绪检测功能。比如,当检测到用户连续使用了感叹号或负面词汇时,自动在 System Prompt 中加入"用户情绪激动,请优先安抚情绪再解决问题"的指令。
六、常见误区
在实际项目中,System Prompt 的设计经常会踩到一些坑。以下是四个最常见的误区及其改进方法:
误区一:System Prompt 越长越好
很多人认为,把所有可能的规则都塞进 System Prompt 就万无一失了。实际上,过长的 Prompt 会稀释模型的注意力——模型在处理长文本时,对开头和结尾的内容记忆更牢,中间部分容易被忽略。这就像一份 50 页的岗位说明书,新员工大概率只记住了前 5 页和最后一页。
| 反模式 | 问题 | 改进 |
|---|---|---|
| 过长的 System Prompt | 占用大量 token,注意力稀释 | 精简到核心指令,建议控制在 500 字以内 |
误区二:角色定义模糊
"你是一个智能助手"是典型的模糊角色定义。这种定义没有告诉模型具体的专业领域、交互风格和能力边界,导致模型在不同问题上的行为风格不一致——有时啰嗦,有时简洁;有时专业,有时随意。
| 反模式 | 问题 | 改进 |
|---|---|---|
| 模糊的角色定义 | 模型行为不一致 | 用具体场景和示例约束,参考五维模型 |
误区三:指令相互矛盾
这是最隐蔽的问题。比如同时写"请简洁回答"和"请详细解释每个概念"——模型不知道该听哪条。这种矛盾往往是因为 Prompt 是多人协作、逐步追加的,每个贡献者加了自己的需求,却没人做整体审查。
| 反模式 | 问题 | 改进 |
|---|---|---|
| 矛盾指令 | "简洁回答" + "详细解释" | 设定明确的优先级,删除冲突项 |
误区四:忽略安全约束
在开发和测试阶段,开发者往往只关注功能是否正常,而忽略了安全约束。等上线后才发现:用户通过精心构造的输入,绕过了业务规则,让模型输出了不该输出的内容。安全约束不是"可有可无"的,它是生产环境的底线。
| 反模式 | 问题 | 改进 |
|---|---|---|
| 忽略安全约束 | 生产环境风险 | 始终包含安全规则层,做红队测试 |
误区五:写完就不改了
System Prompt 不是一锤子买卖。上线后你会发现各种边界情况:某些问题模型答得不好、某些场景模型的行为不符合预期、某些安全约束被绕过了。这些都是迭代优化的信号。好的 System Prompt 是经过数十次甚至上百次微调的结果。
建议建立一个 Prompt 版本管理机制:每次修改都记录变更内容和原因,用回归测试集验证修改后的效果,确保新版本不会引入新的问题。