Agent 1 面试题与答案

每道题分三部分:一句话(先抛结论)→ 详细回答(被追问时展开)→ 可能的追问。
所有数字都已对照 google_docs_assisant/data/real/results_*.json 核对过。


先看:口径统一(面试前必须记住)

容易说错的地方 正确口径
技术栈 全套谷歌:教师 Gemini,学生 Gemma 3 27B
工具数量 17 个:PDF 4(PyMuPDF、pymupdf4llm、pdfplumber、pypdf)、Office 4(python-docx、mammoth、python-pptx、openpyxl)、通用转换 3(MarkItDown、pandoc、plain-text)、5 种解码器(Base64、Hex、摩斯码、URL、ROT13)、OCR 兜底 1(Tesseract)
测试集 79 个文件:arXiv 31 个(pdf 9、扫描 pdf 4、论文转出的 docx / html / md 各 6),维基 48 个(html / txt / md / docx / pptx 各 8,GB18030 4,编码文本 4)
学生 vs 教师 89% 对 86%,只差 2 个文件,说「追平教师、速度快一倍」最稳,不要说「明显超过」

关键数字速查

方案 F1 选对率 平均工具调用 每文件耗时
规则 baseline 0.873 82%(65/79) 4.3 2.0 秒
教师 Gemini 0.884 86%(68/79) 1.2 15.7 秒
Gemma 3 27B 未训练 0.873 82%(65/79) 1.1 9.2 秒
Gemma 3 27B 第 2 轮 SFT(最终) 0.888 89%(70/79) 1.2 8.1 秒
oracle 天花板 0.895 — — —
消融(Gemma 3 4B) F1 选对
未训练 0.833 62/79
第 1 轮 SFT(99 条,只做拒绝采样) 0.854 60/79
第 2 轮 SFT(222 条,修过数据) 0.893 73/79
单次路由对照(用现有数据算出,非真实训练) F1 选对
只用 ReAct 第一个工具 0.818 54/79
每种格式固定用最好的工具(事后挑选,乐观上限) 0.802 68/79

一、项目架构与设计

Q1:请完整介绍 Agent 1 的整体架构、输入输出、核心流程,以及为什么需要使用 Agent。

一句话

Agent 1 把用户上传的任意文件转成干净完整的文本;因为没有一个解析工具在所有文件上都最好,所以我让模型用 ReAct 在 17 个工具里「先试一个、看质量、不好再换」。

详细回答

输入:用户上传的任意文件(pdf、扫描 pdf、docx、pptx、xlsx、html、md、txt、编码文本等)。

输出(交给 Agent 2):

  • 选中工具解析出的全文
  • 选了哪个工具、质量指标
  • 格式警告(比如扩展名和实际内容不符)
  • 网页公式的 LaTeX 列表(正文里用 ⟦F1⟧ 占位)
  • 完整的决策轨迹(给 Agent 4 追查错误用)

核心流程(4 层):

  1. 格式识别(纯规则):看文件头的魔数判断真实格式,不信扩展名;编码文本只给线索、不解码;MD5 哈希直接拦截。输出一张「身份卡」。
  2. 工具池:17 个工具,按格式筛出可用的;缺依赖的工具开局前就剔除。原则是无损优先,OCR 兜底。
  3. 质量指标:不需要标准答案,给每个结果出一份「体检报告」:有效词数、自然度、乾净度、重复率、结构数量。
  4. ReAct 决策:模型每步输出一个 JSON(调工具 / 结束 / 失败),看 observation 决定下一步;有安全阀,保证永远有输出。

为什么需要 Agent:

  • 没有万能工具:PyMuPDF 会压扁表格,pdfplumber 遇到双栏会串行,pandoc 在 html 上残留标签,扫描件只能 OCR。
  • 有些问题只有跑了才知道:同样是 pdf,可能是扫描件;同样是 txt,可能是 ROT13。
  • 规则方案的问题:全部跑一遍再打分,平均 4.3 次调用;遇到新情况要人工改规则,打了一堆补丁选对率也只有 82%。
  • Agent 方案:平均 1.2 次调用,选对率 89%。

可能的追问

  • 为什么格式识别不用模型? 魔数是确定的,规则 100% 准、零成本;模型只用在需要判断的地方。
  • 为什么编码文本不直接解码? 规则会误判,正常英文被当成 ROT13 解码就成了乱码。只给线索,让模型看原文前 300 字再决定。

Q2:你设计了哪些解析质量指标?这些指标如何计算?如何判断解析结果是否合格?

一句话

先列出「坏结果」长什么样,再针对每一种设计一个不需要标准答案的指标;合格与否没有固定阈值,由模型结合指标和预览判断,「什么程度算够好」是从经过标准答案筛选的训练轨迹里学来的。

详细回答

指标设计(代码 agents/parser/quality.py):

坏结果 指标 计算方式
抽少了 content_tokens 有效词数 英文单词(≥2 字母)+ 中文单字 + 数字串的个数
乱码 clean 乾净度 1 − 坏字符数 / 总字符数。坏字符 = �、控制字符、私用区字符;(cid:数字) 每个按 5 个坏字符算
不像人话 natural 自然度 英文:一半看「像不像单词」(长度正常、有元音),一半看虚词比例(the、of 等);中文看常用字比例;按字数加权
重复 duplicate 重复率 重复出现的长行占比
结构丢了 headings / table_rows / math 数标题、表格行、公式符号

为什么虚词能识别 ROT13:ROT13 之后每个「词」仍由字母组成、长度正常,但 the、of、a 全变了,虚词比例接近 0。

一个坑:越长 ≠ 越完整

  • mammoth 把 Word 里的图片转成 base64 塞进输出,长度是正常的 130 倍
  • markitdown 把 PDF 正文误认成表格,加了一堆 |

解决:算指标前用 visible_text() 去掉 HTML 标签、|、base64 图片,只数真正能看到的文字。修完后规则 baseline 的 F1 从 0.806 升到 0.873。

各指标的分工:clean 只抓字符级乱码;没解码的 Base64、ROT13 每个字符都正常,clean 是 1,要靠 natural 抓;残留的 HTML 标签在算指标前被去掉了,任何指标都看不到,只能靠预览。

怎么判断合格:

  • 运行时没有标准答案,算不了 F1,所以没有数值阈值
  • 「干净」好判断:clean 接近 1、natural 高、duplicate 低、预览是正文
  • 「完整」只能估算:有效词数和页数、文件大小对不对得上
  • 停止标准是学出来的:拒绝采样只留 F1 ≥ oracle − 0.01 的轨迹,所以训练数据里每个「直接结束」事后都验证过是对的

规则 baseline 怎么用这些指标打分:

1
2
content = 0.6 × natural + 0.4 × clean      (Excel 只看 clean)
总分 = content × (0.5 + 0.5 × 相对长度) × (1 − 0.5 × 重复率) + 结构加分(最多 0.05) − OCR 扣 0.05

可能的追问

  • 质量指标和 F1 有什么区别? 质量指标不需要答案,解析时给模型用,是「自己检查卷子」;F1 需要答案,评测时给我用,是「老师拿答案批改」。
  • 弱点是什么? 单看一个结果,发现不了「悄悄漏了一小部分」。比如一篇 PDF 用 PyMuPDF 就停了(0.817),最好的 pypdf 是 0.833,差 0.016,算选错。

二、ReAct 与工具调用

Q3:为什么选择 ReAct,而不是单次 LLM 工具路由或 if/else 规则路由?

一句话

单次路由和 if/else 只能根据文件信息「猜」,ReAct 能根据工具的实际输出再决定;很多问题只有跑了才知道,我的对照实验里,允许看结果换工具让 F1 从 0.818 提到 0.888。

详细回答

三种方案对比:

if/else 规则 单次 LLM 路由 ReAct
依据 格式 → 固定工具 看一次文件信息选一个 看实际输出再决定
选错了 无法补救 无法补救 换工具
新情况 改代码 能推理,但看不到结果 能推理,也能看结果

身份卡里看不到的信息:

  • 扫描 pdf 和普通 pdf 的格式一样,只有跑了 PyMuPDF 发现 0 个字才知道
  • pandoc 在 html 上的残留标签,只有看到预览才知道
  • 编码文本解码后可不可读,只有解了才知道

数据支撑(用现有结果文件算的对照组):

方案 F1 选对
ReAct 只用第一个工具(相当于单次 LLM 路由) 0.818 54/79
每种格式固定用最好的工具(相当于最优 if/else,事后挑选) 0.802 68/79
完整 ReAct 0.888 70/79
规则「全部跑一遍再打分」 0.873 65/79,但要调 4.3 次

有 16 个文件调了第二个工具,全部被救回来:4 个扫描件从 0 升到约 0.85;12 个 html 换成 markitdown,比如一个从 0.661 升到 0.952。

优势集中在哪:pdf(ReAct 选对 10/13 vs 固定工具 6/13)和 txt(16/16 vs 12/16)。这两种格式里内容差异最大。

可能的追问(主动承认更加分)

  • ReAct 有没有比固定规则差的地方? 有。docx 上固定用 python-docx 能全对(14/14),ReAct 只对 10 个;html 上一开始就用 markitdown 就行,模型却总是先试 pandoc 再换,多了一次调用。原因很可能是事后纠正的轨迹按「教师习惯 pandoc → markitdown」构造,学生把「先试 pandoc」也学去了。
  • 代价是什么? 平均只多 0.2 次调用,但每次调用要模型推理,单文件 8.1 秒,规则只要 2.0 秒。

Q4:Agent 如何选择下一个工具?Observation 包含什么?如何设计停止条件和兜底策略?

一句话

选哪个工具由模型自主判断,代码只检查动作是否合法;observation 给的是质量指标加开头、中间两段预览;停止靠模型判断「已经够好」,兜底有三层:最多 6 次调用、无效动作超过 2 次退回规则、所有工具都不行就 fail。

详细回答

1. 怎么选下一个工具(没有写死的规则)

依据有三个:

  • 系统提示:只给通用原则,比如「怀疑不完整、有残留标记就再试另一个工具对比」。唯一点名的是「PDF 几乎没字 → OCR」。
  • 工具描述:任务开头的清单里每个工具都有一句描述,比如 pymupdf4llm「表格转成 Markdown 表格」。
  • SFT 学到的经验(最关键):未训练的 Gemma 不知道 html 上 pandoc 不行时该换 markitdown,这来自经过标准答案筛选和纠正的训练轨迹。

实际表现:测试集上调第二个工具的 16 个文件,只有两种组合:pandoc → markitdown(12 个 html)和 pymupdf → OCR(4 个扫描件)。可以说是从数据里学出来、由 observation 触发的隐式规则。

2. Observation 包含什么(react.py 第 96~105 行)

1
2
3
4
5
6
7
{
"tool": "pandoc", "ok": true, "seconds": 0.42,
"quality": {"chars": 48210, "content_tokens": 7350, "natural": 0.91, "clean": 1.0,
"duplicate": 0.01, "headings": 12, "table_rows": 0, "math": 35},
"preview_head": "开头 600 字(原始输出)",
"preview_middle": "正中间 400 字"
}
  • 失败时只有 ok: false 和 error(异常类型 + 信息)
  • 指标用去掉标记的文本算,预览用原始输出:指标防止被长度骗,预览让模型看到指标看不到的问题(如残留标签)
  • 给两段预览:只看开头容易被封面、标题骗
  • 不给全文:一条 observation 只有几百个 token
  • 页数、文件大小在开局的任务描述里,不在 observation 里

3. 停止条件

  • 正常停止:模型判断结果完整干净就 finish,不为比较而多调
  • 硬上限:最多 6 次工具调用

4. 兜底策略

情况 处理
不是合法 JSON、工具不存在、重复调用、finish 选了没调成功的工具 回复「上一步无效:原因」,让它重来
无效动作超过 2 次 放弃模型,用规则 baseline 的选择
文本太少(无文字层) OCR 兜底
哈希、随机二进制、所有工具都失败 模型输出 fail,附上给用户看的原因

保证系统永远有输出。最终 27B 模型在 79 个测试文件上触发规则兜底 0 次。


Q5:ReAct 中的 Thought、Action、Observation 分别是什么?你的 Agent 1 是如何实现完整循环的?

一句话

Thought 是模型的分析,Action 是它要做的动作,Observation 是程序执行后返回的结果;我把 Thought 和 Action 放进同一个 JSON 由模型输出,程序解析后执行工具,再把 Observation 作为 user 消息接回对话,循环到 finish 或 fail。

详细回答

三个概念:

谁产生 在我项目里的样子
Thought 模型 JSON 里的 thought 字段,必填
Action 模型 call_tool / finish / fail 三选一
Observation 程序 工具结果的体检报告 + 预览,以 user 角色接回对话

一次完整循环(agents/parser/react.py 的 parse_react):

1
2
3
4
5
6
7
8
9
1. 构造任务消息:身份卡 + 可用工具清单(+ 纯文本的前 300 字)
2. 循环:
a. 调模型,拿到一段文本
b. 解析 JSON,校验动作合法性
- 不合法 → 无效次数 +1;超过 2 次就退回规则;否则回复「上一步无效」并继续
c. call_tool → 程序真的运行工具 → 生成 observation → 追加到对话
d. finish → 取出所选工具的结果,结束
e. fail → 记录原因,结束
3. 整段对话存成轨迹

真实例子:扫描版论文

步 Thought Action Observation
1 pdf,18 页,文件很大,可能没有文字层,先用 PyMuPDF 试 call_tool pymupdf ok: false,没有输出任何文本
2 没有文字层,需要 OCR call_tool tesseract-ocr 有效词数 6539,natural 0.946,预览是正常正文
3 质量很好,不用再试 finish tesseract-ocr —

为什么不用 API 自带的 function calling:

  1. Gemma 3 没有原生的工具调用 token,本来就是靠提示词输出 JSON
  2. 教师和学生用同一种纯文本格式,教师输出的每个字就是学生要学的每个字
  3. 每一步强制带 thought,学生学到「怎么想」,Agent 4 能追查是哪一步判断错了

代价是要自己校验动作,所以有了安全阀。


三、解析质量与异常处理

Q6:对于扫描 PDF、双栏 PDF、复杂表格、公式混排等难样本,你如何保证解析质量?

一句话

扫描件靠「字数远少于页数」自动识别并换 OCR;双栏通过评测设计交给 Agent 2 处理;表格靠保留结构的工具和结构指标;网页公式在解析前就把 LaTeX 原样取出、用占位符保护。PDF 里的公式和复杂表格目前没有专门方案,这是已知局限。

详细回答

难样本 怎么处理 效果 / 局限
扫描 PDF 无损工具抽出 0 个字或字数远少于页数 → 模型换 Tesseract OCR 4 个扫描件全部选对,F1 约 0.85(OCR 有错字,这是上限)
双栏 PDF 有的工具(如 pdfplumber)会左右串行。F1 设计成不看顺序只看词,整理顺序交给 Agent 2 Agent 1 只保证「别漏、别多」
复杂表格 工具池里有保留表格的工具(pymupdf4llm 转 Markdown 表格);observation 里有 table_rows 计数;PyMuPDF 会把表格压成一堆数字 没有专门的表格识别模型
网页公式 解析前把 MathML 里的 LaTeX 原样取出,正文换成 ⟦F1⟧ 占位符,最后由程序换回,公式不经过任何模型 解决了「工具把公式拆成一行一个字符」的问题
PDF 公式 PDF 里没有公式源码,只能抽到文字形态 已知局限

通用保障:

  • 质量指标去掉标记再计算,防止被 base64 图片、表格符号骗
  • 无损优先,OCR 有错字所以同等条件下扣分
  • 预览给开头和中间两段,防止被封面骗

可能的追问

  • 为什么不用更强的版面分析模型(如 layout 模型或多模态 OCR)? 可以作为工具加进工具池,ReAct 架构不用改;目前的测试集里这类文件少,没有优先做。

Q7:如果解析工具返回乱码、漏页、超时或错误结果,你如何检测、重试并进行错误归因?

一句话

程序把结果分成「抛异常」「跑完没字」「有输出但质量差」三种状态交给模型,模型看报错内容和横向对比来归因;乱码和漏页靠质量指标检测。目前有两个缺口:只有 pandoc 设了超时,同一个工具也不能重试。

详细回答

检测

问题 怎么检测
乱码 clean 低(�、(cid:12));natural 低(没解码、错误解码)
漏页 content_tokens 和 pdf_pages 对不上;和另一个工具的字数比
重复 duplicate 高
抛异常 ok: false + 异常类型和信息(截取 300 字)
没输出 ok: false + 「工具没有输出任何文本」
超时 只有 pandoc 设了 120 秒超时,其他工具没有(缺口)

异常不会让流程崩溃:run_tool() 里 except Exception 把异常转成一条记录,交给模型决策。缺依赖的工具开局前就从清单里剔除,模型不会遇到「没装 pandoc」这类错误。

归因

  1. 看 ok:ok: true + 指标差 → 工具能跑但不适合 → 换工具
  2. 看报错内容:
    • PDF「没有输出任何文本」→ 很可能无文字层 → OCR
    • File is not a zip file → 文件不是真正的 docx 或已损坏 → 看身份卡的格式警告
    • UnicodeDecodeError → 编码问题
  3. 横向对比(最可靠):
    • 只有一个工具出问题 → 工具不适合或有 bug
    • 底层不同的工具都失败 → 文件本身的问题(损坏、加密、扫描件)→ 用 fail 告诉用户
  4. 事后归因:每个文件的完整轨迹都存下来,Agent 4 能追查是哪一步判断错了

重试

  • 目前同一个工具第二次调用会被判为无效动作,所以不能重试,只能换工具
  • 改进方向:
    • 给所有工具统一加超时,超时返回 ok: false 和 Timeout
    • 区分确定性错误(格式不对、无文字层,重试没用)和偶发错误(超时、内存不足),只允许偶发错误重试一次

最难检测的情况:工具没报错,但悄悄漏了一部分。这在 observation 里和「工具不适合」一样,只能靠字数和页数对不上发现。


四、轨迹蒸馏与拒绝采样

Q8:什么是轨迹蒸馏?为什么使用 Gemini 作为教师模型?如何构造 Agent 1 的 SFT 数据?

一句话

轨迹蒸馏是把强模型的「完整决策过程」作为 SFT 数据教给小模型;选 Gemini 当教师,是因为它在训练集上选对率高、能稳定输出合法 JSON,而且和学生 Gemma 同属谷歌一系,输出风格更接近、更好蒸馏;数据由教师在 166 个训练文件上跑 ReAct,经过拒绝采样和三项补救,得到 222 条。

详细回答

概念关系:

词 意思
SFT 训练方法:照着标准答案学
蒸馏 答案来自强模型,不是人写的
轨迹蒸馏 蒸馏的是一整段「想 → 做 → 看」的决策过程,不只是最终答案

教师只被调用(采样),参数不变;只有 Gemma 被训练。

为什么用 Gemini 当教师:

  • 能力够强:测试集上 F1 0.884、选对率 86%,明显高于未训练的 Gemma(0.873、82%),有东西可教
  • 格式稳定:能稳定输出合法 JSON,拒绝采样时因格式问题丢掉的轨迹少
  • 和学生同一体系:Gemma 和 Gemini 都来自谷歌,输出风格更接近,学生更容易学;整条链路(教师、学生、部署)都在谷歌的技术栈里
  • 能大量调用:要在 166 个训练文件上跑轨迹,还要在难类别上每个文件重采样 4 次,需要稳定的 API

选教师的标准:训练集上选对率要高、能稳定输出合法 JSON、能大量调用。

SFT 数据构造(6 步):

  1. 教师做题:166 个训练文件各跑一遍 ReAct(temperature 0),得到 166 条轨迹
  2. 拒绝采样:只留下「最终选对、没触发兜底、没有无效动作」的
  3. 发现问题:难题被删光(论文 docx 只剩 1 条);学到教师「先用 pandoc」的坏习惯;84% 是一步就结束
  4. 难样本重采样:选对率低于 90% 的类别,每个文件用 temperature 0.8 再跑 4 次,184 次里做对了 74 次
  5. 事后纠正:17 个文件 4 次都没对,由程序按「教师习惯的工具 → 最好的工具」真实调用,再让教师根据 observation 写 thought,不许提标准答案
  6. 重新平衡:每个文件最多 3 条;多步轨迹最多复制 3 份,一步就结束的比例降到约 50%;不删简单轨迹

最终 222 条(第 1 轮只做拒绝采样时是 99 条)。

训练集和测试集不重叠:用的是另外的论文和词条;第 2 轮补数据的依据是训练集上教师的错误率,不是测试集分数。


Q9:什么是拒绝采样?你如何筛选教师模型的成功轨迹?为什么拒绝采样会带来数据偏差?

一句话

拒绝采样就是「让教师做题、只留做对的」;我用三个条件筛选(最终选对、没触发兜底、没有无效动作);偏差在于教师恰恰在最难的题上最容易错,于是难题被系统性删掉,留下的数据又简单又带着教师的偏好。

详细回答

三个筛选条件(全部满足才保留):

条件 为什么
最终选对(F1 ≥ oracle − 0.01) 不能让学生学错误的选择
没触发规则兜底 兜底说明模型自己没完成任务
对话里没有无效动作 所有 model 回合都算 loss,包括那句错误输出

第三条为什么要整条丢掉:即使后来改对了,那句「我觉得应该用 pymupdf」(不是 JSON)也会被算进 loss,学生会照着学错误格式。

过程不重要,结果重要:「先试 pandoc → 不好 → 换 markitdown → 选对」要保留,而且最有价值,它教会学生对比。试了不好的工具不算无效动作。

为什么会产生偏差(三个具体表现):

偏差 原因 我的数据里
难度偏差 教师在难题上错误率高,难题被优先删掉 论文转出的 docx 只剩 1 条
策略偏差 留下的是教师「碰巧做对」的轨迹,仍带着教师的习惯 教师在 html / docx 上总是先用 pandoc,而 pandoc 从不是最好的
复杂度偏差 简单题一步就对,最容易保留 84% 是「调 1 次就结束」,学生学不会对比

偏差的后果(有实验证据):第 1 轮只做拒绝采样,4B 学生 F1 从 0.833 只升到 0.854,选对数反而从 62 降到 60。

一句话总结:拒绝采样保证了「对」,但没保证「全」和「难」。


Q10:你提到事后纠正和难样本重采样,具体怎么实现?如何验证这些方法有效?

一句话

难样本重采样是在教师错误率高的类别上用高 temperature 多采几次、留下做对的;事后纠正是对仍然做不对的文件,由程序用标准答案构造正确的工具路径,再让教师基于真实 observation 补写理由;验证靠第 1 轮和第 2 轮的对照,4B 学生 F1 从 0.854 升到 0.893,选对数从 60 升到 73。

详细回答

难样本重采样(scripts/augment_agent1.py 的 sample 命令):

  1. 统计训练集上每个类别的教师选对率,找出低于 90% 的类别
  2. 这些类别里的每个文件让教师重新跑 4 次
  3. temperature 从 0 调到 0.8:让教师走不同的路,第一次选错的,换条路可能就对了
  4. 用同样的拒绝采样条件筛选
  5. 结果:184 次里做对了 74 次

注意:这一步只是采样,没有任何训练。

事后纠正(全程由程序自动完成,不是人工标注):

  1. 用标准答案算出这个文件上最好的工具(oracle 工具)
  2. 按「教师习惯先用的工具 → 最好的工具 → finish」的顺序,真的调用工具,拿到真实 observation
  3. 把 observation 交给教师,让它只根据 observation 里看得到的证据写每一步的 thought
  4. thought 里不许提到标准答案:学生部署时看不到答案,否则它学到的是「凭空知道答案」
  5. 一共补了 17 个文件

为什么先走一遍教师习惯的工具:让轨迹里有「先试 pandoc → 发现有残留标签 → 换 markitdown」的对比,学生学到的是「不好时要会换」,而不是凭空跳到正确答案。

验证方法

4B 学生 训练数据 F1 选对
未训练 — 0.833 62/79
第 1 轮 99 条,只做拒绝采样 0.854 60/79
第 2 轮 222 条,加上三项补救 0.893 73/79

两轮之间只改了数据,模型、超参数、评测都一样,所以提升可以归因到数据修复。

还没做、但应该做的(主动说):三项补救是一起加的,没有拆开做单项消融。如果被追问,可以说下一步会分别只加重采样、只加事后纠正、只加上采样,看各自的贡献。

可能的追问

  • 事后纠正会不会是在给结论找理由? 有这个风险:路径是程序定的,如果 observation 里看不出明显差别,教师写的 thought 就是在为结论找证据。缓解办法:要求 thought 引用 observation 里的具体证据,可以加一步程序检查,确认引用的数字或文字真的在 observation 里。
  • 副作用? 学生把「先试 pandoc」也学去了,html 上每次多一次调用(见 Q3)。

五、SFT 与 LoRA 训练

Q11:为什么要进行 SFT 轨迹蒸馏?相比直接调用教师模型,训练自己的 27B 模型有什么价值?

一句话

未训练的 Gemma 和规则一样好,不知道什么时候该换工具,所以要训练;训练后的本地 27B 效果追平教师,但速度快一倍、没有额度和费用问题、数据不出本地,还能和其他 Agent 共用一个基座。

详细回答

为什么要训练:

模型 F1 选对
Gemma 3 27B 未训练 0.873 65/79(和规则一样)
Gemma 3 27B 训练后 0.888 70/79

未训练的模型不知道「pandoc 在 html 上会残留标签」「什么时候该换工具」。

本地模型 vs 直接调教师:

教师 Gemini 本地 Gemma 27B
F1 / 选对率 0.884 / 86% 0.888 / 89%
每文件耗时 15.7 秒 8.1 秒
每文件输出 token 约 761(思考模型) 约 160
额度 / 费用 按调用计费,有限流 自己的 GPU,没有限流
数据隐私 文件内容要发给第三方 不出本地
可控性 模型版本可能变 版本固定,可以继续训练

学生为什么能追平教师:学的不是教师的原始表现,而是清洗过的教师:做错的扔掉了,难题补上了,坏习惯被对比轨迹纠正了。

系统层面的价值:Agent 1~4 共用一个 Gemma 27B 基座,vLLM 部署一份,每个 Agent 挂自己的 LoRA。

可能的追问

  • 只为 Agent 1 的话,有必要用 27B 吗? 不一定。4B 第 2 轮的结果是 F1 0.893、选对 73/79、每文件 3.7 秒,和 27B(0.888、70/79)在误差范围内。选 27B 是因为 Agent 2~4 的任务更难,需要 27B,四个 Agent 共用基座。如果 Agent 1 单独部署,4B 更划算。
  • 为什么不做 GRPO? 工具选择的决策空间小,SFT 后 F1 离天花板只差 0.007;RL 的预算留给更难的 Agent 2~4。补充:Agent 1 的工具输出是确定性的,可以预先缓存,环境本身不贵,真正贵的是 27B 的 rollout。

Q12:LoRA 的原理是什么?你的训练数据如何组织?如何训练模型正确输出工具调用?

一句话

LoRA 冻结原模型,只训练两个低秩小矩阵的乘积作为权重增量;数据是多轮对话的 JSONL,observation 用 user 角色;只对模型回合算 loss,加上训练数据里没有一条格式错误,模型就学会了稳定输出合法 JSON。

详细回答

LoRA 原理:

1
2
新权重 = W(冻结)+ (alpha / r) × A × B
A:d × r,B:r × d,r 远小于 d
  • 原来一个 d × d 的矩阵要训练 d² 个参数,LoRA 只训练 2 × d × r 个
  • rank r = 16,alpha = 32(缩放系数 alpha / r = 2),dropout 0.05
  • QLoRA:基座用 4-bit 加载,只训练 LoRA,单卡 48GB 能训 27B,约 50 分钟
  • 学习率 1e-4,3 个 epoch

加在哪些层:语言模型 62 层,每层的全部 7 个线性层(q / k / v / o、gate / up / down)。

  • 原因:QLoRA 论文发现加到所有线性层才能接近全参数微调;任务既要「会看」(注意力),也要「会判断」(MLP)
  • 参数量约 1.1 亿,占 27B 的 0.4%,适配器 454 MB
  • 不加视觉编码器(输入全是文字)、词嵌入和输出层(没有新词,常规不加)、LayerNorm(参数少、收益小)
  • 这个选择没有做过对比实验,是按 QLoRA 论文和常用做法定的

为什么用 LoRA:单卡装不下全参数训练;4 个 Agent 共用一个基座;只有 222 条数据,全参数训练容易过拟合。

训练数据组织(data/sft/agent1_train_r2.jsonl):

1
2
3
4
5
6
7
8
9
10
11
12
{
"messages": [
{"role": "system", "content": "你是一个文档解析 agent……"},
{"role": "user", "content": "请解析这个文件:{身份卡 + 工具清单}"},
{"role": "assistant", "content": "{\"thought\": \"...\", \"action\": \"call_tool\", \"tool\": \"pandoc\"}"},
{"role": "user", "content": "observation:\n{...}"},
{"role": "assistant", "content": "{\"thought\": \"...\", \"action\": \"call_tool\", \"tool\": \"markitdown\"}"},
{"role": "user", "content": "observation:\n{...}"},
{"role": "assistant", "content": "{\"thought\": \"...\", \"action\": \"finish\", \"choice\": \"markitdown\"}"}
],
"file": "...", "category": "...", "source": "..."
}
  • file / category / source 是备注,不参与训练
  • observation 用 user 角色:对模型来说它和用户的话一样,都是外部给的信息
  • Gemma 没有 system 角色,模板把 system 内容拼到第一条 user 开头;assistant 在 Gemma 里叫 model

怎么训练模型正确输出工具调用:

  1. 训练时不调任何工具:所有 observation 都是教师当初录下来的,整段对话一次性喂进去(「看录像学」)
  2. 只对 model 回合算 loss:system、user、observation 的 label 设为 −100,PyTorch 会跳过。学了 observation 反而会让模型去编造 observation
  3. <end_of_turn> 也算 loss:模型要学会说完一个动作就停,把控制权交给程序
  4. 实现(train/sft_agent1.py 第 43~44 行):每个 model 回合渲染两次聊天模板,full(含这一轮)减去 ctx(这一轮之前)就是要算 loss 的部分,并断言模板满足前缀性质
  5. 数据里没有一条格式错误:拒绝采样把含无效动作的轨迹整条删掉
  6. 推理时还有校验:不合法就让它重来,超过 2 次退回规则

效果:最终 27B 模型在 79 个测试文件上触发规则兜底 0 次。

部署:vLLM 加载 FP8 基座,--enable-lora 挂载 LoRA,不合并。需要合并时在 CPU 上加载 bf16 完整基座合并,不在 4-bit 基座上合并(会把量化误差带进权重)。注意训练用 4-bit 基座、部署用 FP8,两者有轻微不一致。


六、实验指标与消融

Q13:你项目中的 F1 是如何定义和计算的?Baseline、教师模型和蒸馏模型如何保证公平对比?

一句话

F1 是不看顺序的「词袋」重合:每个词取两边次数的较小值作为重合数,再算召回率和精确率;公平性靠同一份测试集、同一套工具和 ReAct 代码、同一个 oracle 和评分脚本,测试集和训练集完全不重叠。

详细回答

F1 计算(benchmarks/agent1/metrics.py):

  1. 全部转小写;把 PDF 断行的连字符接回去(optimiza- 换行 tion → optimization)
  2. 切词:英文按单词(至少 2 个字母),中文一个字算一个,一串数字算一个
  3. 统计每个词出现的次数
  4. 计算:
1
2
3
4
5
重合数 = Σ min(输出里该词的次数, 标准答案里该词的次数)
召回率 = 重合数 / 标准答案总词数
精确率 = 重合数 / 输出总词数
F1 = 2 × 召回率 × 精确率 / (召回率 + 精确率)
整体 F1 = 79 个文件的 F1 取平均

例子:标准答案「the model learns the policy」,输出「the the model policy page 3」→ 重合 4,召回 4/5 = 0.80,精确 4/6 = 0.67,F1 ≈ 0.73。

为什么不看顺序:双栏论文会被读串行,顺序乱了但字都在;整理顺序是 Agent 2 的事。

标准答案从哪来:arXiv 论文的 LaTeX 源码转纯文本;维基百科用官方接口拿纯正文。不是人工标注,又准又便宜。

oracle 和选对率:

  • 单个文件的 oracle = 所有工具在这个文件上的最高 F1
  • 选对 ⇔ 所选工具的 F1 ≥ oracle − 0.01(留 0.01 余量:好几个工具效果几乎一样时,选哪个都不算错)
  • 选对率看「选得对不对」,F1 看「内容好不好」,要一起看。比如扫描件选对率 100%,但 F1 只有 0.85

公平对比:

保证项 做法
同一份考卷 都在同样的 79 个文件上评测
同一套工具 工具实现、预处理(包括公式占位符)完全一样
同一套流程 教师、未训练 Gemma、训练后 Gemma 用同一份 ReAct 代码和系统提示,temperature 都是 0
同一把尺子 同一个评分脚本、同一个 oracle(所有结果文件里 baseline 0.873、oracle 0.895 完全一致)
没有数据泄露 训练集用另外的论文和词条;第 2 轮补数据依据的是训练集上教师的错误率,不是测试集分数

需要说明的口径问题:

  • 维基网页 F1 只有约 0.38:不是解析得差,标准答案只有正文,网页里的信息框、参考文献、导航栏都会被抽出来,精确率天然低。这一类主要看召回率(接近 1.0)。
  • 耗时不完全可比:教师走网络 API,本地模型走 vLLM,耗时差异里有网络和硬件的因素。
  • 样本量小:79 个文件,而且同一篇论文的几种格式是相关的;差距小于 0.01 的结论不可靠。
  • 规则 baseline 的补丁:如果修 baseline 时看过测试集,会让 baseline 偏高,这对模型是更严格的对照,不是放水。

Q14:如何证明轨迹蒸馏和 ReAct 真正有效?你会如何设计消融实验?

一句话

已经做的对照有三组:规则 vs 未训练 vs 训练后(证明训练有效)、第 1 轮 vs 第 2 轮数据(证明数据修复有效)、只用第一个工具 vs 完整 ReAct(证明多步有效);还应该补做三项数据补救的单项消融和观测信息的消融。

详细回答

已经做过的

要证明什么 对照 结果
模型比规则好 规则 vs 训练后 27B F1 0.873 → 0.888,选对 65 → 70,调用 4.3 → 1.2
训练有效 未训练 27B vs 训练后 27B F1 0.873 → 0.888
数据修复有效 4B 第 1 轮 vs 第 2 轮(只改数据) F1 0.854 → 0.893,选对 60 → 73
只做拒绝采样不够 4B 未训练 vs 第 1 轮 F1 +0.02,选对反而 62 → 60
多步 ReAct 有效 只用第一个工具 vs 完整 ReAct F1 0.818 → 0.888,16 个多步文件全部救回
比单次路由好 每种格式固定用最好的工具 vs ReAct F1 0.802 vs 0.888
模型大小的影响 4B 第 2 轮 vs 27B 第 2 轮 0.893 vs 0.888,在误差范围内

还应该补做的消融

消融 做法 想回答的问题
数据补救单项消融 分别只加重采样 / 只加事后纠正 / 只加上采样 三招各贡献多少
去掉 thought 训练数据只保留 action 「边想边做」本身有没有用
去掉预览 observation 只给指标 预览对识别残留标签有多重要
去掉中间预览 只给开头一段 防封面欺骗的设计是否有效
去掉质量指标 observation 只给预览 指标的价值
上采样比例 一步就结束的比例取 84% / 50% / 30% 比例怎么定最好
多次随机种子 每个设置训练 3 次 差距是否超过随机波动

统计显著性:79 个文件的差距要做配对检验(比如 bootstrap 或 McNemar 检验选对率),差距小于 0.01 的不下结论。


七、工程落地与系统优化

Q15:如果需要处理十万份不同类型的文档,你会如何优化 Agent 1 的延迟、Token 消耗、稳定性和吞吐量?

一句话

核心思路是「能不用模型就不用」:按格式分流,md、pptx、docx 这类固定工具就够的直接走规则,只把 pdf、txt、html 交给模型;再加上文件哈希缓存、系统提示前缀缓存、工具并行和超时、OCR 单独排队,把模型调用和 token 消耗降一个数量级。

详细回答

先估算现状:每个文件约 2.2 次模型调用、约 2900 个输入 token、160 个输出 token。十万份文档约 2.9 亿输入 token、1600 万输出 token;如果单路串行,每份 8 秒要跑 9 天。

1. 延迟和 token:减少模型调用

优化 依据 预期效果
按格式分流 md、pptx 上固定工具和 ReAct 一样好;docx 固定用 python-docx 反而更好(14/14 vs 10/14) 测试集里约 45% 的文件可以跳过模型
文件哈希缓存 同一个文件重复上传很常见 重复文件零成本
前缀缓存 系统提示和工具描述每次都一样,占输入的大头 vLLM 开 prefix caching,输入 token 的计算量大幅下降
换小模型 4B 第 2 轮效果和 27B 在误差范围内,每文件 3.7 秒 吞吐提升数倍
去掉多余的步骤 html 上「先 pandoc 再 markitdown」多了一步 修训练数据,或在 html 上首选 markitdown
observation 精简 预览 600 + 400 字可以按需缩短 输入 token 减少

2. 吞吐量:并行与分层

  • 模型推理:vLLM 连续批处理,多个文件的 ReAct 循环交错执行,GPU 不空等工具
  • 工具执行:进程池并行,CPU 密集的解析和 GPU 推理分开
  • OCR 单独排队:OCR 一个文件要 20~30 秒,放到独立的工作池(可以用 GPU OCR),不阻塞快速文件
  • 大文件先抽几页做快速判断(是否有文字层),再决定全量用哪个工具

3. 稳定性

  • 所有工具统一加超时(现在只有 pandoc 有),超时当作失败交给模型
  • 偶发错误允许重试一次;确定性错误不重试
  • 每个工具在独立子进程里运行,崩溃不影响主流程
  • 任务队列 + 失败重投 + 死信队列,保证幂等(同一个文件哈希只处理一次)
  • 模型服务挂了 → 自动退回规则 baseline(F1 0.873,仍可用)

4. 监控和持续改进

  • 监控:各格式的工具选择分布、规则兜底率、fail 率、平均调用次数、OCR 占比
  • 抽样质检:每天随机抽一批,找强模型或人工检查
  • 新出现的错误类型 → 加入训练集 → 定期重训 LoRA

可能的追问

  • 按格式分流会不会损失 ReAct 的优势? 不会。ReAct 的优势集中在 pdf 和 txt(同一格式下内容差异大),md、pptx、docx 上没有优势,甚至更差。分流只是把模型用在真正需要的地方。

站内搜索

没有找到内容!