System Prompt vs User Prompt:一个管「你是谁」,一个管「你要什么」
🦞 一只用 AI Agent 搭副业产线的程序员
上篇我们说了 Prompt 要当代码写。但代码需要架构。
这篇文章给你 Prompt 架构的第一层:System Prompt 和 User Prompt 的分工。很多人混在一起用,结果就是 Prompt 一长就失控。
别混。这两个角色天然不同。
先搞清楚:AI 收到的不是一段话,是两个人
每次调用 API,你传的Messages数组里有一个role字段。可以填三种:
| Role | 谁说的 | 干什么的 |
|---|---|---|
system | 你(开发者) | 定义 AI 的行为规则、角色、边界 |
user | 用户 | 具体的任务、问题、输入 |
assistant | AI | 历史回复(多轮对话用) |
这三者的关系像什么?
System Prompt = 员工手册(入职第一天发的,管所有行为) User Prompt = 任务单(每天变化的,管这一个任务) Assistant = 工作日志(之前干了啥,管上下文连续性)一个反例:混在一起会怎样
很多人这样写 Prompt:
你是一个优秀的 Go 后端开发,有 10 年经验,擅长写高性能代码。 你的回答要简洁、准确、代码完整可运行。不要写废话。 现在帮我写一个 LRU 缓存。 哦对了,用泛型。要有测试。还有……这有什么问题?角色定义和具体任务搅在一起。每次新任务,你都要把角色定义再说一遍。想复用角色设定?复制粘贴。想换一个角色?全删了重写。
更糟的是——如果你在一个循环 Agent 里用这种 Prompt,每一轮都要重复发送角色描述。10 轮对话,角色重复了 10 次,全是钱的流失。
正确的分层
// System Prompt —— 一次定义,永久复用systemPrompt:=`你是 Go 后端开发专家。 行为规则: 1. 代码完整可编译,不省略 import 和 package 声明 2. 所有公开函数包含 godoc 注释 3. 不确定的 API 明确说"不确定",不编造 4. 不要解释代码为什么这样写,除非被问到 输出约束: - 代码块使用 ```go标记-错误处理不要用panic,用 fmt.Errorf 包装-时间相关的逻辑使用 time.Time,不要用字符串`// User Prompt —— 每次不同,只管任务userPrompt:=`用 Go 泛型实现一个线程安全的 LRU 缓存。 要求: - 支持 Get、Put、Len 方法 - 容量满了淘汰最久未使用的 - 并发安全`这样分开后,System Prompt 你可以复用 100 次、1000 次。User Prompt 只管这一次的任务。
用 JSON 配置驱动 Prompt 管理
更进一步——把 System Prompt 从代码里抽出来,用配置文件管理:
{"roles":{"go-backend-dev":{"name":"Go 后端开发专家","system":"你是 Go 后端开发专家。代码完整可编译,不确定的 API 明确说不知道。输出简洁精准。","temperature":0.1,"max_tokens":500},"code-reviewer":{"name":"代码审查员","system":"你是代码审查员。只找问题,不表扬。按严重程度排序:高危 > 中危 > 低危。每个问题标注行号。","temperature":0.0,"max_tokens":800},"doc-writer":{"name":"技术文档写手","system":"你是技术文档写手。写中文 godoc 风格注释。准确、简洁、不说废话。","temperature":0.3,"max_tokens":1000},"rubber-duck":{"name":"小黄鸭调试助手","system":"你是小黄鸭调试助手。用户会向你解释代码逻辑。你的任务是通过提问帮他发现思维盲区。不要直接给答案。用苏格拉底式提问。","temperature":0.7,"max_tokens":300}}}然后在 Go 代码里加载:
packagemainimport("encoding/json""fmt""os")typeRoleConfigstruct{Namestring`json:"name"`Systemstring`json:"system"`Temperaturefloat64`json:"temperature"`MaxTokensint`json:"max_tokens"`}typeConfigstruct{Rolesmap[string]RoleConfig`json:"roles"`}typePromptManagerstruct{config*Config}funcNewPromptManager(configPathstring)(*PromptManager,error){data,err:=os.ReadFile(configPath)iferr!=nil{returnnil,err}varcfg Config json.Unmarshal(data,&cfg)return&PromptManager{config:&cfg},nil}func(pm*PromptManager)BuildMessages(roleKeystring,userPromptstring)([]Message,float64,int,error){role,ok:=pm.config.Roles[roleKey]if!ok{returnnil,0,0,fmt.Errorf("未知角色: %s",roleKey)}messages:=[]Message{{Role:"system",Content:role.System},{Role:"user",Content:userPrompt},}returnmessages,role.Temperature,role.MaxTokens,nil}funcmain(){pm,_:=NewPromptManager("roles.json")// 换角色只需要换一个 keymessages,temp,maxTok,_:=pm.BuildMessages("go-backend-dev","写一个 WebSocket 心跳检测函数")result:=callLLM(messages,temp,maxTok)fmt.Println(result)// 同一个代码库,不同的角色messages2,temp2,maxTok2,_:=pm.BuildMessages("code-reviewer",result)// 把刚生成的代码丢给审查员review:=callLLM(messages2,temp2,maxTok2)fmt.Println(review)}切换角色现在是一行代码的事。团队里不同的人可以复用同一个角色库。
System Prompt 写什么的 4 层模型
| 层 | 内容 | 示例 |
|---|---|---|
| 第 1 层:角色定义 | AI 是什么身份 | 「你是 Go 后端开发专家」 |
| 第 2 层:行为规则 | 怎么做事 | 「代码完整可编译,不确定的说不知道」 |
| 第 3 层:输出约束 | 输出什么格式 | 「用 ```go 代码块,不输出解释」 |
| 第 4 层:禁止事项 | 不要做什么 | 「不要编造 API,不要用 panic」 |
第 1 层是必须的,第 2-4 层根据任务复杂度选配。简单任务可能只需要第 1 层 + 第 3 层。
一个真实的坑:System Prompt 太长
我一开始给 Agent 写的 System Prompt 有 3000 字,规定了各种场景下的行为。结果呢?
AI 经常「忘记」后面的规则。因为 System Prompt 太长,离得远的规则权重降低了。
后来我优化成 200 字以内,只保留最重要的 3-4 条规则。准确率反而提高了。
System Prompt 不是越长越好。是越精越好。
User Prompt 怎么写:三段式模板
[任务] 用 Go 实现一个带过期时间的本地缓存。 [输入] 无 [输出] 完整可编译的 Go 代码,包含: - Set(key, value, ttl) 方法 - Get(key) (value, bool) 方法 - 过期自动清理机制三段式:任务 + 输入 + 输出。简洁明确,不给 AI 发挥空间。
总结:一张表分清 System 和 User
| System Prompt | User Prompt | |
|---|---|---|
| 谁写 | 你(开发者) | 你(或最终用户) |
| 频率 | 写一次,复用多次 | 每次任务不同 |
| 内容 | 角色、规则、约束 | 具体任务、输入数据 |
| 维护方式 | JSON 配置文件 | 代码里拼接或模板 |
| 长度 | 尽量短,200 字内 | 根据任务复杂度 |
| 类比 | 员工手册 | 任务单 |
下一篇我们搞一个实验——同一个任务,用 Few-shot(给示例)和 Zero-shot(不给示例)各跑 50 次,统计准确率差异。告诉你什么时候该给示例,什么时候给了反而坏事。
关注我,别错过。
🦞 一只用 AI Agent 搭副业产线的程序员
全平台同名:虾哥不加班
需要定制 AI 工具?来聊聊 → lob_ai源码:GitHub - lobster-bujiaban
