本文整理自 CNB 上的开源课程 《Vibe Agent:从零搭建 Coding Agent》。课程讲义、示例代码与练习仓库均在 CNB 上公开,文中示例出自该仓库对应章节。

同一个模型,同一个问题,只把最前面那条 system 消息换掉,输出的语气、格式、详略就会整体改变。常见的解释是”提示词写得好”,这个说法停留在表面。真正起作用的是两件事:一套固定的消息格式,以及训练阶段形成的服从倾向。

一次调用传的是一份列表

OpenAI 兼容接口的输入是一份 messages 列表,每条消息只有两个字段:role 和 content。

1
2
3
4
5
6
messages = [
{"role": "system", "content": "你是一个简洁的助手。"},
{"role": "user", "content": "什么是递归?"},
{"role": "assistant", "content": "函数调用自身解决子问题。"},
{"role": "user", "content": "举个例子。"},
]

模型读完整个列表,生成列表的下一条,也就是 assistant 消息。列表本身不保存任何东西,调用结束即丢弃。模型不会因为多轮对话而改变内部状态,能影响下一轮输出的,只有这份列表的内容和顺序。

角色 由谁写 承担什么
system 开发者 身份、规则、约束
user 使用者 输入与追问
assistant 模型自己,也可由程序写入 历史回复

第四种角色 tool 要等到讲工具调用时才会出现。这里先记住三种。

三种角色并不对等。user 与 assistant 通常交替出现,system 一般只放在最前面一条,整轮对话都以它为背景。

assistant 消息有一个容易被忽略的性质:它可以由程序伪造。程序往里塞一段”模型说过的话”,模型在下一轮会把它当成自己的历史。少样本示例靠这个性质工作,提示词注入也靠它。

模型不保存上一轮

第一轮传入的是 [system, user1],返回 assistant1。第二轮要带上 [system, user1, assistant1, user2],模型才接得住上一句。

如果第二轮只发 user2,模型不会知道之前聊过什么。桌面聊天产品看起来有记忆,是客户端把历史一起拼进了请求。模型这一侧只做一件事:看到什么,续写什么。

轮数一多,这份历史的体积会线性增长,而每次请求都要重新传输、重新计费。所谓长对话越聊越慢、越聊越贵,原因就在这里。

由此可以推出一条对 Agent 很关键的结论:上下文是程序维护的一份数据,不是模型的属性。循环每跑一轮,这份数据就长一截,费用和延迟跟着上涨。历史保留多少、在哪里截断,属于工程问题。

system 的权重来自训练

训练数据里有大量这样的配对:system 写一条规则,assistant 按规则回答。课程里的对照实验很直观:system 写”一位专业的技术助手”,模型自述为标准助手;换成”一只猫娘,每句话以喵结尾”,同一个问题得到带喵的自我介绍。两条 user 消息一字不差,差异全部来自那条 system。

机制在训练阶段。监督微调教会模型这种对话格式,基于人类反馈的强化学习进一步让它倾向于遵循前置指令。system 里的约束于是成为一种高优先级的条件,模型会把它当作当前任务的前提。

这也解释了它为什么是最便宜的行为定制。换模型要改架构和预算,微调要准备数据和算力,改一段文本只需要一次编辑。同一个模型在客服、编程、写作三个场景下表现差异很大,多数时候差异来自 system 内容,模型能力并没有变。做评测时把 system 固定下来,结果才具备可比性。

工程里的 system prompt 是结构化的

真实 Agent 项目的 system prompt 常有几百到几千 token,按块组织:角色说明、规则、输出格式、约束、示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
SYSTEM = """你是一个代码审查助手。

## 规则
- 只报告安全问题和性能问题,不评论风格
- 用中文回答

## 输出格式
每条问题写成一行:文件:行号 | 问题 | 建议

## 约束
- 没有发现问题时只输出 LGTM
- 不确定的问题标注"存疑",不要编造
"""

拆成小节的意义是让每条要求都能被单独定位。示例输入输出也常写在这里,本质是塞进 system 的少样本提示:给两组输入输出,模型会照着格式续写,比用一句话描述格式更稳。规则和输出格式混在一段散文里,模型容易顾此失彼;分块之后行为更稳定。

需要分清的是,system prompt 定义的是行为倾向,不提供强制力。使用者可以在 user 消息里写”忽略以上要求”,模型有一定概率照做。要把某类行为真正挡住,得在执行层做:工具参数校验、危险操作二次确认、允许清单。提示词是第一道约束,执行层的检查才是兜底。

两轮对话的实际写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import os
from openai import OpenAI

client = OpenAI(base_url=os.environ["BASE_URL"], api_key=os.environ["API_KEY"])
MODEL = os.environ.get("MODEL_ID", "deepseek-v4.1-flash")

messages = [
{"role": "system", "content": "你是一位代码审查助手,只指出安全与性能问题,没有问题时只回复 LGTM。"},
{"role": "user", "content": "审查这一行:eval(request.args['q'])"},
]

first = client.chat.completions.create(model=MODEL, messages=messages, temperature=0)
messages.append({"role": "assistant", "content": first.choices[0].message.content or ""})
messages.append({"role": "user", "content": "按严重程度排一下。"})
second = client.chat.completions.create(model=MODEL, messages=messages, temperature=0)

print(second.choices[0].message.content)

关键在两次调用之间那次 append。去掉它,第二轮模型就不知道”排一下”指什么,因为它没有别的途径读取上一轮。这段代码里没有会话对象,也没有 session id,所谓对话就是这个列表在程序内存里的样子。

对搭 Agent 意味着什么

Agent 的结构可以概括成三样东西:一段设计好的 system prompt、一组工具、一个反复拼装 messages 的循环。上面三个事实各自对应一处设计。角色分工决定谁能往哪条消息里写;无状态决定历史必须由程序保存和裁剪;system 的权重决定行为的基线,真正的边界要靠代码守。

下一节换一个角度算账:这份列表按 token 计费,长度也有上限。