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 层):
- 格式识别(纯规则):看文件头的魔数判断真实格式,不信扩展名;编码文本只给线索、不解码;MD5 哈希直接拦截。输出一张「身份卡」。
- 工具池:17 个工具,按格式筛出可用的;缺依赖的工具开局前就剔除。原则是无损优先,OCR 兜底。
- 质量指标:不需要标准答案,给每个结果出一份「体检报告」:有效词数、自然度、乾净度、重复率、结构数量。
- 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 | content = 0.6 × natural + 0.4 × clean (Excel 只看 clean) |
可能的追问
- 质量指标和 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 | { |
- 失败时只有
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 | 1. 构造任务消息:身份卡 + 可用工具清单(+ 纯文本的前 300 字) |
真实例子:扫描版论文
| 步 | 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:
- Gemma 3 没有原生的工具调用 token,本来就是靠提示词输出 JSON
- 教师和学生用同一种纯文本格式,教师输出的每个字就是学生要学的每个字
- 每一步强制带 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」这类错误。
归因
- 看 ok:
ok: true+ 指标差 → 工具能跑但不适合 → 换工具 - 看报错内容:
- PDF「没有输出任何文本」→ 很可能无文字层 → OCR
File is not a zip file→ 文件不是真正的 docx 或已损坏 → 看身份卡的格式警告UnicodeDecodeError→ 编码问题
- 横向对比(最可靠):
- 只有一个工具出问题 → 工具不适合或有 bug
- 底层不同的工具都失败 → 文件本身的问题(损坏、加密、扫描件)→ 用 fail 告诉用户
- 事后归因:每个文件的完整轨迹都存下来,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 步):
- 教师做题:166 个训练文件各跑一遍 ReAct(temperature 0),得到 166 条轨迹
- 拒绝采样:只留下「最终选对、没触发兜底、没有无效动作」的
- 发现问题:难题被删光(论文 docx 只剩 1 条);学到教师「先用 pandoc」的坏习惯;84% 是一步就结束
- 难样本重采样:选对率低于 90% 的类别,每个文件用 temperature 0.8 再跑 4 次,184 次里做对了 74 次
- 事后纠正:17 个文件 4 次都没对,由程序按「教师习惯的工具 → 最好的工具」真实调用,再让教师根据 observation 写 thought,不许提标准答案
- 重新平衡:每个文件最多 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 命令):
- 统计训练集上每个类别的教师选对率,找出低于 90% 的类别
- 这些类别里的每个文件让教师重新跑 4 次
- temperature 从 0 调到 0.8:让教师走不同的路,第一次选错的,换条路可能就对了
- 用同样的拒绝采样条件筛选
- 结果:184 次里做对了 74 次
注意:这一步只是采样,没有任何训练。
事后纠正(全程由程序自动完成,不是人工标注):
- 用标准答案算出这个文件上最好的工具(oracle 工具)
- 按「教师习惯先用的工具 → 最好的工具 → finish」的顺序,真的调用工具,拿到真实 observation
- 把 observation 交给教师,让它只根据 observation 里看得到的证据写每一步的 thought
- thought 里不许提到标准答案:学生部署时看不到答案,否则它学到的是「凭空知道答案」
- 一共补了 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 | 新权重 = W(冻结)+ (alpha / r) × A × B |
- 原来一个 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 | { |
file/category/source是备注,不参与训练- observation 用 user 角色:对模型来说它和用户的话一样,都是外部给的信息
- Gemma 没有 system 角色,模板把 system 内容拼到第一条 user 开头;assistant 在 Gemma 里叫
model
怎么训练模型正确输出工具调用:
- 训练时不调任何工具:所有 observation 都是教师当初录下来的,整段对话一次性喂进去(「看录像学」)
- 只对 model 回合算 loss:system、user、observation 的 label 设为 −100,PyTorch 会跳过。学了 observation 反而会让模型去编造 observation
<end_of_turn>也算 loss:模型要学会说完一个动作就停,把控制权交给程序- 实现(
train/sft_agent1.py第 43~44 行):每个 model 回合渲染两次聊天模板,full(含这一轮)减去ctx(这一轮之前)就是要算 loss 的部分,并断言模板满足前缀性质 - 数据里没有一条格式错误:拒绝采样把含无效动作的轨迹整条删掉
- 推理时还有校验:不合法就让它重来,超过 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):
- 全部转小写;把 PDF 断行的连字符接回去(
optimiza-换行tion→optimization) - 切词:英文按单词(至少 2 个字母),中文一个字算一个,一串数字算一个
- 统计每个词出现的次数
- 计算:
1 | 重合数 = Σ min(输出里该词的次数, 标准答案里该词的次数) |
例子:标准答案「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 上没有优势,甚至更差。分流只是把模型用在真正需要的地方。