跳到主要内容

第 2 章:协议焊在权重里,prompt 薄得反常

上一章你可能在想:模型怎么知道要按 <Analyze>→<Code>→<Answer> 这套格式输出?既没有 system prompt 教它,prompt 里也没写工具说明。答案是:这套协议不在 prompt 里,在模型权重里。本章拆这个设计。

2.1 五个标签是真·特殊 token

<Analyze></Analyze> 这些不是普通字符串,而是被加进 tokenizer 词表的特殊 token。训练前要先跑 add_vocab.py 把它们注入基座模型。

deepanalyze/add_vocab.py:78-90 加的十个 token:

开标签闭标签语义
<Analyze></Analyze>想:这步该干什么
<Understand></Understand>读执行结果后的观察总结
<Code></Code>要执行的 Python
<Execute></Execute>执行结果(由驱动填,模型不生成)
<Answer></Answer>最终答案 / 报告

注入方式很标准(add_vocab.py:40-46):tokenizer.add_tokens(new_tokens) 然后 model.resize_token_embeddings(len(tokenizer))

2.2 巧妙之处:用 <think> 的嵌入初始化

新加的 token 默认是随机嵌入,等于让模型从零学这五个符号。DeepAnalyze 不想从零开始——它的基座是 DeepSeek-R1-0528-Qwen3-8B,一个已经会用 <think>/</think> 思考的推理模型。

add_vocab.py 的 docstring(add_vocab.py:26-28)明说意图:

Initialize <Analyze> and </Analyze> embeddings using <think> and </think>.

直觉:<Analyze>(分析/思考)和基座原有的 <think> 语义最近,拿 <think> 的嵌入当起点,新 token 一上来就「站在思考能力的肩膀上」,后续 SFT 收敛更快。

诚实说明:add_vocab.py 当前代码主要做了 add_tokens + resize,docstring 描述的「用 <think> 嵌入逐个初始化」这步在本文件里看不到显式拷贝嵌入的代码——意图写在注释里,实际拷贝可能发生在仓库未包含的脚本或被简化掉了。(inferred) 这里以注释陈述的设计意图为准。

脚本最后还会拿一段含全部标签的样例 encode 一遍打印 token 切分(add_vocab.py:95-108),验证这些标签确实被切成单个 token 而不是被拆成 <Analyze> 一串字符——这是「特殊 token 注入成功」的判据。

2.3 为什么要焊进权重(而不是写 prompt)

把协议做成 token + 训进权重,和「在 system prompt 里写一段格式说明」相比:

  • 输出稳定:<Code> 是一个原子 token,模型不会写成 < Code > 或漏闭合,外层正则才敢这么简单。
  • 外层极薄:不需要在 prompt 里塞工具 schema / few-shot,prepare_vllm_messages 只拼两段(下一节)。
  • 代价是绑死:协议和模型一体,换基座、加新标签都得重训。属于「用通用性换可靠性」的取舍。

横向对比:很多 function-calling agent 把协议放在可换的 schema/system prompt 里(灵活、可热插拔工具);DeepAnalyze 反过来,把协议焊死换稳定——这是 data-agents 这种「循环固定、就是写代码-执行」场景才划算的赌注。

2.4 prompt 模板薄到反常

既然协议在权重里,prompt 就没什么可写的了。看生产版 API/utils.py:prepare_vllm_messages(API/utils.py:91-127)拼出来的最终用户消息:

# Instruction
<用户原话,比如 Generate a data science report.>

# Data
File 1:
{"name": "person.csv", "size": "10.6KB"}

File 2:
{"name": "enrolled.csv", "size": "20.4KB"}
...

就这两段。没有 system prompt、没有工具列表、没有「你是一个数据分析师」的人设

# Data 那段叫数据缩略图(data thumbnail),由 collect_file_info 生成(API/utils.py:71-86):遍历工作目录,每个文件只记 {name, size},不读内容。原因前面说过——逼模型自己写 pd.read_csv 去探索,这正是 agent 行为的来源。如果把内容塞进 prompt,模型就退化成「一次性看完直接答」,不再是 agent。

2.5 execute 这个非标准角色

回灌执行结果时,各版本用的 message 角色都是 "execute",而非 OpenAI 标准的 user/assistant/tool:

  • 参考版:messages.append({"role": "execute", "content": exe_output})(deepanalyze.py:139)。
  • API 版:同样 {"role": "execute", "content": exe_output}(API/chat_api.py:224341)。
  • RL 版:{"role": "execute", "content": f"\n<Execute>\n{observation}\n<Execute>"}(deepanalyze_env.py:225-228)。

这要求模型的 chat template 认识 execute 角色,把它渲染成 <Execute>...</Execute> 包裹的观察块。这是协议焊进权重的又一个体现:execute 角色和五个标签是配套训练出来的,换别的模型直接喂会渲染错。

边界提示:RL 版那行 <Execute> 的闭标签写成了 <Execute> 而不是 </Execute>(deepanalyze_env.py:227),看起来像个小笔误;但因为奖励判定主要看 <Analyze>/<Answer> 是否成对(见 04 章),不一定影响训练。(inferred)

2.6 代码地图

主题文件符号
特殊 token 列表deepanalyze/add_vocab.pynew_tokens(10 个标签)
token 注入deepanalyze/add_vocab.pyload_and_extend_model(add_tokens + resize_token_embeddings)
<think> 初始化意图deepanalyze/add_vocab.pydocstring load_and_extend_model
prompt 模板API/utils.pyprepare_vllm_messages
数据缩略图API/utils.pycollect_file_info
非标准 execute 角色deepanalyze.py / API/chat_api.py{"role": "execute"}