16 项目怎么讲
前面十几篇讲的每个知识点,面试时最终都要落到”你在项目里怎么做的”。同一个项目,讲成”我用 LangGraph 和 RAG 做了一个 Agent”,和讲成”我先定义了不依赖模型自述的验收标准,然后评测时发现验收自己错了两次”,面试官的判断完全不同。
这篇要讲清楚:1 分钟、3 分钟、10 分钟三个版本的讲稿;每个技术决策为什么这么做;项目里所有能说的数字、出处和算法;主动讲局限怎么讲;简历上每一句话背后的证据和追问;30 个高频追问的回答方向。所有数字都来自 eval/REPORT.md、结果文件、runs.db 和 git 历史,文中有一个脚本可以把核心数字从原始文件里重新算一遍。
怎么读:
前置:整个系列。本篇里提到的每个知识点,都在前面对应的篇目里有详细讲解。
第一部分 速记页
项目一句话:CrossBuild Agent,给一个开源 C/C++ 仓库,Agent 自己调工具把它交叉编译到 aarch64-linux-musl,由程序独立验收产物。
数字卡片(背下来,每个都要能说出出处):
| 数字 | 含义 | 出处 |
|---|---|---|
| 5 个工具 | list_files、read_file、write_file、run_command、search_knowledge | agent/tools.py、build_agent.py |
| 10 + 5 个项目 | 评测集 10 个分三档(简单 4、中等 4、困难 2),留出集 5 个 | eval/projects*.json |
| 25 → 40 步 | 步数上限;只改这一项,libpng 29 步、re2 32 步通过(各一次) | REPORT 第一版 |
| 9/10、10/10 | 第一版 baseline 和 rag 的通过数,不能归因于知识库 | REPORT 第一版 |
| 3 次 | 第一版两百多次工具调用里只查了 3 次知识库 | REPORT 第一版 |
| 2 次 | 验收自身的 bug:空构建判通过;构建目录写死 build/ | REPORT 重跑、留出集 |
| 45 次 | 留出集因验收 bug 作废重跑的运行数 | REPORT 留出集 |
| 30/30、29/30 | v2 baseline 和 rag;rag 失败那次是路径检查误拦 | REPORT v2 |
| 8 → 0 | toolchain 改环境变量后,“子目录找不到 toolchain”次数 | REPORT v3 |
| 60/60 | v3 两个配置全部通过,通过率失去区分度 | REPORT v3 |
| 15 条、48 条 | 知识库条目数、检索测试集查询数(四类) | REPORT 知识库 |
| 45/48 | 混合检索 R@1,最高;重排同分、每条慢 30 多倍 | REPORT 知识库 |
| 15/15 × 3 | 留出集三种配置全过;步数 14.2 / 15.7 / 16.8,差异在一倍标准差内 | REPORT 留出集 |
| 8 次 | 留出集 30 次开知识库的运行只调用 8 次检索 | REPORT 留出集 |
| 30/30、29/29 | 手写版和 LangGraph 版同配置;另有 1 次 402 余额不足不计 | REPORT LangGraph |
| 65 行、126 行 | 手写循环和 LangGraph 版的循环代码 | REPORT LangGraph |
| 第 12 步杀、第 13 步续 | run 217 断点续跑,第 18 步通过,工具调用无重复 | REPORT LangGraph |
| 288 次、41.6 倍 | runs.db 总运行数;输入 token 是输出的倍数 | runs.db |
| 21 个 | 单元测试数 | tests/ |
三个最有价值的故事(面试时优先讲):
- 验收标准写错了两次:只看退出码,空构建也判通过;构建目录写死,正确结果判失败
- “90% → 100%“不能归因:三条证据推翻了”知识库有效”的直觉
cat绕过凭据防护:两个工具功能重叠,防护只有弱的那个那么强
第二部分 易混对照
讲项目时最容易说错、说过头的地方:
| 容易说成 | 实际应该说 | 为什么 |
|---|---|---|
| ”加了知识库,通过率从 90% 提到 100%" | "第一版 90% 到 100%,但证据不支持归因于知识库;留出集上没有观察到可测量的收益” | 步数方向混杂、只查 3 次、救回的查询库里没有 |
| ”知识库没用" | "未能证明有效,瓶颈在经验覆盖和调用率,不在检索算法” | 没证明有效 ≠ 证明无效 |
| ”实现了沙箱" | "做了工具层检查,明确不是沙箱;构建脚本能绕过,要容器才行” | README 原文 |
| ”LangGraph 版效果更好" | "两版结果看不出差别,LangGraph 多的是检查点和续跑能力” | 30/30 vs 29/29,步数 token 相近 |
| ”混合检索效果最好,所以用了" | "离线检索测试上混合最好;但端到端没带来可测量收益” | 离线分数高估实际作用 |
| ”步数少了,说明变好了" | "差距在一倍标准差内,只能算趋势” | 样本少、波动大 |
| ”Agent 成功率 100%" | "评测集上全部通过,说明评测集太简单、饱和了” | 通过率无区分度 |
| ”续跑不会重复执行工具" | "检查点之间能正确恢复;节点执行中途被杀会重复执行,需要幂等” | 检查点粒度是节点 |
| ”平均步数 13.1” | 先说清是哪一版、包不包括失败的运行 | 第一版 REPORT 的 13.1 含失败运行,只算通过的是 11.8 |
| ”测试覆盖了所有情况" | "21 个测试覆盖越界、误删、验收、续跑等回归场景;没有注入测试” | 如实 |
第三部分 面试口述稿
3.1 一分钟版(自我介绍里带到项目)
我做了一个叫 CrossBuild Agent 的项目。给它一个开源的 C/C++ 仓库,它自己读项目结构、执行 cmake 命令、看报错、改配置,把项目交叉编译到 ARM64 的 musl 平台。选这个题目是因为我工作里做过二十多款工具的 ARM64 适配,知道这件事重复性高、又不是固定流程,适合用 Agent 做。
我在这个项目里最看重的是评测。成功与否不看模型怎么说,而是由程序重新构建一遍,检查产物是不是目标架构的 ELF 文件。我用 10 个项目分三档加 5 个留出项目,每个配置跑三轮,比较了加不加知识库、BM25 和向量混合检索、手写循环和 LangGraph。
过程中比较有价值的发现是:验收标准本身错过两次,都是读运行轨迹发现的;还有第一版里通过率从 90% 到 100% 的提升,我分析后认为不能归因于知识库,后面用留出集验证也确实没有可测量的收益。
3.2 三分钟版(“介绍一下你的项目”)
这个项目是一个做交叉编译的 Agent。背景是我工作里做过二十多款 C/C++ 工具的 ARM64 适配,还移植过 PyTorch 到 musl 平台,每个项目的依赖、编译选项、报错都不一样,没法写成固定脚本,但排查思路是有规律的,所以想试试让 Agent 来做。
怎么做的。 Agent 有五个工具:列目录、分页读文件、写文件、执行命令、查经验库。执行命令有白名单、路径检查和超时。循环是我手写的,有步数上限和卡住检测。结束后由程序独立验收:重新构建,并且在构建目录里找到 aarch64 架构的 ELF 产物才算成功。后来我又用 LangGraph 重写了一版循环,加了检查点和断点续跑,工具也封装成了 MCP server。
评测。 10 个开源项目分三档,另外 5 个完全没跑过的项目当留出集,每个配置跑三轮。第一版每配置只跑了一轮,看到加知识库后通过率从 9 个到 10 个,但我没有直接下结论,因为步数变化四个变好四个变差,知识库两百多次调用里只被查了 3 次,被救回的那个项目查的东西知识库里根本没有。所以结论写的是”未能证明有效”。
踩过的坑。 最大的教训是验收自己会错。第一次,旧验收只看 cmake 的退出码,复查时发现有两次运行一个目标都没编译也判了通过;第二次,留出集上 libarchive 九次失败八次,查下来是仓库自带一个叫 build 的源码目录,Agent 换了个目录名编译成功了,验收却写死去 build 目录找。这次 45 次结果全部作废重跑。另外还发现过一个安全问题,命令白名单里有 cat,参数又不检查路径,读文件工具上加的凭据防护被整个绕过了。
结论。 改完验收和工具缺陷后,两个配置 60 次全部通过,评测集已经没有区分度了;留出集上三种配置也都全过,知识库没带来可测量的收益,瓶颈在经验覆盖不足和模型很少主动查。LangGraph 和手写版结果一样,它多出来的价值是检查点和续跑。
3.3 十分钟版(技术面深挖,按顺序讲,每段一两分钟)
① 问题定义和为什么用 Agent
交叉编译每个项目路径都不同:有的关掉测试就能过,有的要先编依赖,有的依赖库 API 变了要打补丁。步骤没法事先定死,要根据报错决定下一步,所以用 Agent 而不是工作流。
我先定的是成功标准。不能让模型说”编译成功”就算,要程序独立判断。最早定的是重新执行
cmake --build返回 0,后来发现不够,改成必须在构建目录里找到目标架构的 ELF 产物,静态库要拆开检查,CMake 自己在配置阶段生成的探测程序不算。
② Agent 循环和工具设计
循环是标准的:发消息给模型,模型返回工具调用,执行,结果作为 tool 消息放回去,直到模型不再调工具或者到步数上限。加了两个保护:一是步数上限,二是卡住检测,同一个工具调用(工具名加参数完全相同)重复超过 3 次就不再执行,所有调用都陷入重复时判定卡住;写文件成功之后命令的计数清零,因为改了文件再跑同一条命令是合理的。
工具设计上改过几次。读文件原来只保留末尾 100 行,长的 CMakeLists 前半部分模型看不到,第一版里有 6 次运行、15 次模型自己写 CMake 脚本分段读文件绕过去;改成从开头分页读之后,后面 120 次运行一次都没出现。命令输出截断时退出码固定放在第一行,完整输出存到工作目录里可以翻页读。报错信息也写成能指导下一步的,比如越界时说明”主机上的库不能用于交叉编译”。
③ 评测设计和两个验收 bug
评测集 10 个项目按依赖复杂度分三档。第一版每配置一轮,后来改成三轮,因为同一个项目三次跑的步数标准差平均有两三步,单次对比没有意义。判断差异我用了一个保守的规则:差距不到一倍标准差只算趋势。
两个验收 bug 前面说过。我从中得到的教训是,评测里最该怀疑的是评分器,而且这类问题只能靠读轨迹发现,看总分是看不出来的。每修一个我都补了回归测试。
④ 第一版的归因分析和步数实验
第一版 baseline 9 个通过、rag 10 个。我给出了三条不能归因的证据。困难档那两个项目三轮里五次跑满 25 步,我看 libpng 的轨迹,它走的路是对的,识别出缺 zlib、去交叉编译 zlib,只是到第 25 步才刚开始配 zlib。所以我单独只改步数上限到 40,libpng 29 步、re2 32 步都过了。这是那一版里唯一的单变量实验,但每边只跑一次,只能说支持这个判断。
⑤ 知识库和检索对比
知识库从 160 次运行记录里提炼,按报错签名分组,让模型根据证据写候选条目,我逐条对着运行记录审核,7 条候选收了 5 条、合并 1 条、丢弃 1 条,最后 15 条。
检索测试集扩到 48 条,分四类:原始报错、见过的报错、刻意不用原词的改写描述、没见过的报错变体。结果 BM25 在原始报错上满分但改写类最弱,向量检索改写类最好但带具体库名时会排错,RRF 混合总分最高 45/48;加重排总分一样,每条查询从 33 毫秒变成 1.1 秒,所以没用。
⑥ 留出集:为什么换项目,结果是什么
新增的经验是从原来 10 个项目的运行记录里提炼的,再用这 10 个项目测就等于提前给了答案,所以另选了 5 个 Agent 从没跑过的项目。三种配置各三轮,全部通过,步数 14.2、15.7、16.8,差异在一倍标准差以内,方向还是开了知识库略多。30 次开知识库的运行只调了 8 次检索,查得最多的是 libarchive 的链接错误,库里根本没有这类经验。结论是瓶颈在覆盖,不在检索算法;离线检索测试是按已有条目写的,高估了实际作用。
⑦ LangGraph 重写和续跑
为了公平,两版共用工具、提示词、重复检测和验收,只换循环组织方式。结果看不出差别。LangGraph 代码多了一倍,但检查点有实际价值:我在编 re2 第 12 步时 kill 掉进程,续跑从第 13 步开始,直接用已经装好的 abseil,第 18 步编译通过,工具调用没有重复。要说明的是,杀的时机正好在节点执行完之后;如果杀在工具节点执行一半,恢复后已执行的工具会再跑一次,不能重复的操作要另外做幂等。
⑧ 安全和边界
评测跑 re2 时看到 Agent 执行
ls /opt/homebrew/Cellar想找主机上的 abseil,而且执行成功了。查下来命令参数不检查路径,白名单里还有 cat,实测能读/etc/hosts,读文件工具上加的凭据拦截被绕过。修复是参数路径也检查、去掉 cat。后来又把字符串前缀比较改成按路径层级比较。但我在 README 里明确写了这不是沙箱。cmake、make 本身能执行任意代码,评测里模型就写过 CMakeLists 用
execute_process读主机环境,参数检查管不到。要编不可信的仓库得放容器里。
⑨ 后端和可观测
FastAPI 加 SSE 推进度,Agent 在后台线程跑,事件经队列推给前端。同时只允许一个构建,拿不到锁返回 409。修过一个 bug:锁原来在响应生成器里释放,客户端一断开锁就放了,后台构建还在跑。改成在构建线程结束时释放,加了回归测试。每次运行和每次工具调用都记在 SQLite 里,前面说的那些发现基本都是翻这些记录找到的。
⑩ 局限和下一步
主动说几个没做好的:评测集已经饱和,需要依赖链更深、非 CMake 的项目;每配置三轮样本仍然少;没有容器隔离,子进程还能读到环境变量里的 API key;没记缓存命中和模型耗时,算不出真实成本;知识库要么扩大覆盖,要么改成报错后自动检索,而不是靠模型主动调用。
3.4 “你为什么做这个项目?”
两个原因。一是和我的工作经验直接相关,我在德科信息负责 C++ 工具的 ARM64 移植,做过二十多款工具和 PyTorch 运行时的适配,交叉编译里的坑我是真踩过的,所以我能判断 Agent 做得对不对,也能设计有意义的评测。二是这个任务的结果可以用程序客观验证,不需要人工打分或者模型打分,评测数字比较经得起追问,我想借这个项目把 Agent 的评测方法认真做一遍。
3.5 “项目里最难的是什么?”
我觉得最难的不是写代码,而是判断结果到底说明了什么。
比如第一版通过率从 90% 到 100%,最自然的写法是”加知识库提升了 10 个百分点”。但我去看了逐项目的步数、知识库的调用次数和查询内容,发现证据不支持。后来改验收、重跑、换留出集,每一步都在推翻或修正前面的结论,报告也改过好几次,还把早期写错的数字就地改正了。
另一个难点是评分器本身会错,而且错得很隐蔽。空构建判通过这个问题,总分上完全看不出来,是我复查复验输出时才发现是空的。
3.6 “如果重新做,你会怎么改?”
三点。第一,一开始就把验收标准写严并补测试,我前期的很多结果因为验收标准变了不得不作废重跑。第二,评测集一开始就选难一点的项目,并且从第一版起每个配置跑多轮,否则通过率很快饱和、单轮结果又没法比较。第三,工具执行一开始就放进容器,而不是先做工具层检查再发现挡不住。
第四部分 逐个详解
4.1 讲项目的结构
先说 STAR 是怎么来的。 早年的面试常问”你的优点是什么""如果遇到某某情况你会怎么做”,候选人很容易说漂亮话,面试官也很难判断真假。工业与组织心理学里有一条被广泛接受的经验:一个人过去在类似情况下怎么做,最能预测他以后会怎么做。于是有了行为面试,只问”你实际做过什么”。美国人才测评公司 DDI 说,STAR 是他们 1974 年随行为面试方法 Targeted Selection 一起提出的,用来让候选人把一段经历讲完整、让面试官追问时有章可循(DDI 的说明,这是 DDI 自己的说法)。技术面问”讲讲你的项目”,本质上也是行为面试。
STAR 是讲经历的常用结构:情境(Situation)、任务(Task)、行动(Action)、结果(Result)。用在技术项目上,我建议扩展成下面这样,重点放在”判断”上:
flowchart LR
S[背景<br/>为什么做] --> T[目标<br/>成功标准是什么]
T --> A[做法<br/>关键设计和理由]
A --> E[验证<br/>怎么评测 数字]
E --> F[发现<br/>意外 失败 修正]
F --> L[局限<br/>没做好的 下一步]
| 部分 | 要讲什么 | 常见错误 |
|---|---|---|
| 背景 | 为什么做、和自己经历的联系 | 空泛(“AI 很火”) |
| 目标 | 可验证的成功标准 | 没有标准,或者用模型自述当标准 |
| 做法 | 关键设计和为什么 | 罗列技术名词 |
| 验证 | 评测方法、样本量、数字 | 只有一个数,不说怎么来的 |
| 发现 | 意外、失败、自己推翻过的结论 | 只讲成功 |
| 局限 | 主动说没做好的和原因 | 不说,等面试官问出来 |
面试官判断”做过”还是”看过”,主要看后三部分。 能说出”我原以为……后来发现……所以改成……”的,几乎不可能是编的。
4.2 数字从哪来:自己能复算
面试官追问数字时,最怕说不清”怎么算的”。下面这个脚本从项目的原始结果文件和 runs.db 里把核心数字重新算一遍。保存为 recheck.py,在项目根目录运行。
import json
import sqlite3
from pathlib import Path
from statistics import mean
EVAL = Path("eval")
def load(pattern):
runs = []
for f in sorted(EVAL.glob(pattern)):
runs.extend(json.loads(f.read_text()))
return runs
def summary(name, runs, tiers=None):
picked = [r for r in runs if tiers is None or r["tier"] in tiers]
valid = [r for r in picked if not r["status"].startswith("crash")]
passed = [r for r in valid if r["passed"]]
steps = mean(r["steps"] for r in passed) if passed else 0
print(f"{name:<28} 有效 {len(valid):>2} 通过 {len(passed):>2} 平均步数 {steps:.1f}")
summary("第一版 baseline", load("result_baseline.json"))
summary("第一版 rag", load("result_rag.json"))
summary("v2 baseline", load("result_v2_baseline_*.json"))
summary("v2 rag", load("result_v2_rag_*.json"))
summary("v3 baseline", load("result_v3_baseline_*.json"))
summary("v3 rag", load("result_v3_rag_*.json"))
summary("v3 baseline 简单+中等", load("result_v3_baseline_*.json"), {"简单", "中等"})
summary("LangGraph baseline 简单+中等", load("result_lg_baseline_*.json"), {"简单", "中等"})
summary("LangGraph baseline", load("result_lg_baseline_*.json"))
for cfg in ("baseline", "bm25", "hybrid"):
summary(f"留出集 {cfg}", load(f"result_ho_{cfg}_*.json"))
conn = sqlite3.connect("file:runs.db?mode=ro", uri=True)
n, tin, tout = conn.execute("SELECT COUNT(*), SUM(prompt_tokens), SUM(completion_tokens) FROM runs").fetchone()
print(f"runs.db:{n} 次运行,输入 {tin:,},输出 {tout:,},比例 {tin / tout:.1f}")
实际输出(Python 3.14,只用标准库):
第一版 baseline 有效 10 通过 9 平均步数 11.8
第一版 rag 有效 10 通过 10 平均步数 12.4
v2 baseline 有效 30 通过 30 平均步数 14.7
v2 rag 有效 30 通过 29 平均步数 12.7
v3 baseline 有效 30 通过 30 平均步数 13.6
v3 rag 有效 30 通过 30 平均步数 13.2
v3 baseline 简单+中等 有效 24 通过 24 平均步数 10.8
LangGraph baseline 简单+中等 有效 24 通过 24 平均步数 10.5
LangGraph baseline 有效 29 通过 29 平均步数 13.4
留出集 baseline 有效 15 通过 15 平均步数 14.2
留出集 bm25 有效 15 通过 15 平均步数 15.7
留出集 hybrid 有效 15 通过 15 平均步数 16.8
runs.db:288 次运行,输入 48,704,845,输出 1,169,783,比例 41.6
逐段讲。
Path("eval").glob("result_v3_baseline_*.json"):按通配符找文件,*匹配任意字符,于是_1、_2、_3三轮的结果文件都会被找到sorted(...):让文件按名字顺序读,结果可复现runs.extend(列表):把另一个列表的元素逐个追加进来。和append的区别是:append会把整个列表当成一个元素加进去from statistics import mean:求平均值tiers=None:不传就不按难度档筛选;传一个集合(set){"简单", "中等"}时,r["tier"] in tiers判断是否属于其中之一r["status"].startswith("crash"):崩溃的运行(比如 402 余额不足)不计入有效运行mean(... for r in passed) if passed else 0:只对通过的运行算平均步数;没有通过的就返回 0,避免对空序列求平均报错- 最后一段用只读模式打开
runs.db,10 篇讲过
和 REPORT 对照:
| 脚本输出 | REPORT | 是否一致 |
|---|---|---|
| v2 rag 29/30、v3 各 30/30 | 同 | 一致 |
| v3 baseline 简单+中等 10.8、LangGraph 10.5 | LangGraph 对比表 10.8、10.5 | 一致 |
| LangGraph 有效 29 | ”第 3 轮 re2 … 402 … 不计入” | 一致 |
| 留出集 14.2 / 15.7 / 16.8 | 同 | 一致 |
| 288 次、41.6 | 进度文档记录 | 一致 |
| 第一版 baseline 平均步数 11.8 | REPORT 通过率表写 13.1 | 算法不同 |
最后一行值得专门说:REPORT 第一版的平均步数 13.1 是 10 个项目全部算进去,包括失败时跑满 25 步的 re2;脚本只算通过的 9 个,得到 11.8。两个数都对,但讲的时候必须说清是哪个。面试时说出一个数字,要能回答”这个数怎么算的”。
自己改一改:
- 给
summary加一个参数,切换”只算通过的”和”全部算”,验证第一版 baseline 的 13.1 - 算一下 v3 baseline 困难档(
{"困难"})的平均步数,和 REPORT 的 25.0 对照
4.3 技术决策清单:每个”为什么”
面试官最爱问”为什么这么做”。下面每个决策都给出:做了什么、理由、证据、代价。理由要和证据对得上,不能事后编一个好听的。
| 决策 | 理由 | 证据或出处 | 代价 |
|---|---|---|---|
| 用 Agent 而不是固定脚本 | 每个项目依赖、选项、报错不同,下一步取决于上一步的报错 | 评测集三档项目的轨迹差异很大;困难项目要先编依赖 | 不稳定、贵,需要评测 |
| 程序独立验收 | 模型会说”编译成功”但实际没有;要可复现、不依赖人工或模型打分 | 旧标准下 json 两次空构建判通过,说明连退出码都不够 | 验收自身会错,要测试 |
| 验收要求目标架构 ELF 产物 | 退出码为 0 不代表编译了东西;主机编译也可能返回 0 | tests/test_regressions.py 的 Verify 测了空项目、主机构建、架构不对都判失败 | 要解析 ELF、拆静态库 |
| 手写循环起步 | 单 Agent 线性任务,几十行就够,每一步可控可调试 | 65 行 | 没有检查点 |
| 步数上限 40 | 困难项目是嵌套任务,25 步不够 | 只改上限,libpng 29 步、re2 32 步通过 | 失败时成本上升;本质是愿意为失败付多少钱 |
read_file 从开头分页 | 旧版只留末尾 100 行,前半部分看不到 | 15 次绕路 → 0 次 | 模型要多调几次 |
| 截断输出时保留退出码 | 截断后看不出命令成没成功 | 回归测试 test_exit_code_survives_truncation_and_log_is_readable | — |
| toolchain 改环境变量 | 模型在子目录传相对路径找不到 toolchain,还被路径检查误拦 | 8 次 → 0;误拦消失;这是 v2 → v3 唯一的改动 | 依赖 CMake 3.21+ |
| 知识库做成工具让模型调用 | 只在需要时查,不占上下文 | — | 模型很少主动调用(第一版 3 次、留出集 8 次) |
| BM25 起步 | 条目少(最初 8 条)、查询多是带关键词的报错原文,关键词匹配就够,而且快、没有依赖 | 48 条测试里 BM25 原始报错类 15/15,每条 0.1ms | 改写描述类弱(13/17) |
| 用混合检索、不用重排 | 混合总分最高;重排总分一样但慢 30 多倍 | 45/48;33ms vs 1126ms | — |
| 留出集 | 经验是从原 10 个项目提炼的,再用它们测等于提前给答案 | 5 个新项目 | 又发现一个验收 bug,45 次作废重跑 |
| 每配置 3 轮 | 同一项目重复跑步数波动大,单轮对比没意义 | 同项目 3 次标准差平均 2.5 到 3.4 步 | 成本三倍 |
| 用 LangGraph 重写 | 验证框架实际带来什么,而不是凭印象;岗位要求常见 | 结果无差别;续跑实测成功 | 代码翻倍、线程问题 |
| 两版共用工具和验收 | 保证对比公平,只变循环写法 | check_and_run 共用 | — |
| 工具封装成 MCP server | 同一套工具和边界检查给其他客户端用 | tests/test_mcp.py | 客户端的确认和审计不受控 |
| 工具层检查但不做容器 | 学习项目、工具链在 macOS 主机上;明确写出边界 | README “不是安全沙箱” | 构建脚本能绕过;子进程能读环境变量 |
| 同时只允许一个构建 | Agent 设置进程级环境变量,工作目录按项目名 | 409;锁在构建线程结束时释放 | 只能单用户 |
关于”为什么 BM25 起步”要注意:上表的理由是根据项目实际情况整理的,和最初的知识库规模、查询特点对得上。面试时如果被追问”当时是怎么想的”,按事实说”条目少、报错文本关键词明显,先用最简单的,后面再用测试集对比”,不要说成事先做过调研。
4.4 主动讲局限
主动说局限不是示弱,是展示判断力。关键是说清楚原因和影响,最好带上”如果要做会怎么做”。
| 局限 | 原因 | 影响 | 如果继续做 |
|---|---|---|---|
| 评测集饱和 | 10 个项目里 8 个关掉测试就能过;修正验收后全部通过 | 通过率无区分度,只能比步数和 token | 加依赖链更深、非 CMake、需要 try_run 的项目 |
| 样本少 | 一轮完整评测十分钟左右、上百万 token | 大部分差异只能算趋势 | 更多轮次;配对比较和置信区间(09 篇实验) |
| 知识库没带来可测量收益 | 覆盖不足、模型很少主动查 | 投入的检索工作没有端到端回报 | 报错后自动检索附在结果后;扩大覆盖(但不能用留出集的经验) |
| 不是沙箱 | 工具层检查拦不住构建脚本 | 不能编不可信仓库 | 容器、断网、不传环境变量 |
| 子进程能读 API key | subprocess.run 没有限制环境变量 | 恶意构建脚本能拿到 key | 只传必要环境变量;容器 |
| 没记缓存命中和模型耗时 | 最初只记了 token 总数 | 算不出真实成本和延迟分解 | 按步记录 usage 全字段 |
| 没有告警 | 单机项目 | 余额耗尽后又连续崩了 4 个项目 | 连续崩溃告警、余额监控 |
| SSE 没有心跳 | 没在 Nginx 后部署过 | 部署后长命令可能断连 | 定期发注释行 |
| 只有单用户 | 进程级环境变量、全局锁 | 不能多人用 | 任务队列、每任务独立环境 |
| 通过率只跑了一个模型 | 成本 | 不知道换模型结论是否成立 | 换模型跑同一评测 |
讲的时候的句式:“这个我没做好,原因是……,它的影响是……,如果继续做我会……”。
4.5 简历逐条对照
简历(resume/resume_ai_v1.md)上 CrossBuild Agent 的每一条,都要知道证据在哪、会被怎么追问。
| 简历原文要点 | 证据 | 最可能的追问 | 回答要点 |
|---|---|---|---|
| 建模为多步决策任务;程序独立验收,要求构建通过且产出目标架构 ELF,不采信模型自述,也不依赖人工或模型打分 | build_agent.verify;REPORT 重跑一节 | ”验收会不会错?“ | 错过两次,讲两个 bug 和修复、回归测试 |
| 手写 Tool Calling 循环,含步数预算、重复调用检测与卡死判定 | build_events | ”卡死怎么判定?“ | 工具名加参数完全相同算同一个调用,超过 3 次就不再执行、直接提示换思路;出现过的不同调用超过 2 种、且每一种都超过上限时判卡住并结束;写文件成功后清掉命令的计数 |
| LangGraph 重写,SQLite 检查点支持断点续跑,两版同配置各 3 轮结果一致 | graph_agent.py;REPORT LangGraph | ”一致是什么意思?""续跑会重复执行吗?“ | 30/30 vs 29/29(1 次 402 不计),步数 token 相近;节点粒度,中途被杀会重复 |
| 路径层级校验、命令白名单、超时终止,输出截断保留退出码并落盘分页 | tools.py | ”安全吗?“ | 讲 cat 绕过;明确不是沙箱 |
| toolchain 改环境变量后,子目录配置找不到工具链由 8 次降为 0 | REPORT v3 | ”8 次是怎么统计的?“ | v2 两个配置各 4 次,从 trace 里按报错统计;v3 唯一改动所以因果清楚 |
| 工具封装为 MCP server | mcp_server.py、tests/test_mcp.py | ”MCP 是什么?为什么做?“ | 12 篇;复用同一套工具和边界 |
| 从 160 次运行记录提炼报错经验并人工审核,知识库扩至 15 条 | REPORT 知识库;knowledge/mine_runs.py | ”怎么提炼的?审核标准?“ | 按报错签名分组 → 模型写候选 → 对照运行记录审核;7 条收 5 合 1 丢 1 |
| 48 条分类查询对比,混合 Recall@1 45/48 最高,重排同分但慢 30 余倍,未采用 | REPORT 检索对比 | ”为什么分类?""RRF 是什么?“ | 分类才看出互补;RRF 按排名融合,k=60 |
| 10 个项目三档加 5 个留出项目,每配置重复 3 轮 | eval/ | ”为什么要留出集?“ | 防泄漏 |
| 修正空构建误判、构建目录写死两处验收问题后重跑 | REPORT | ”怎么发现的?“ | 复验输出为空;libarchive 9 败 8 报错一致 |
| 留出集上知识库未带来可测量收益,定位瓶颈为经验覆盖不足而非检索算法 | REPORT 留出集 | ”那做 RAG 有什么意义?“ | 方法跑通、定位了瓶颈、给出了改进方向;如实结论比虚高数字有价值 |
| SQLite 记录每步工具调用、耗时与 token,界面经 SSE 实时展示与回放 | trace.py、server/app.py、Vue 组件 | ”记了什么?没记什么?“ | 10 篇;没记缓存命中、模型耗时 |
| 21 个测试覆盖越界、误删、验收与续跑 | tests/ 下 4 个文件 | ”误删是什么?“ | prepare 拒绝 .. 这类项目名,否则会删到工作区外 |
4.6 关于”代码是你自己写的吗""做了多久”
这两个问题在 2026 年的面试里越来越常见,要提前想好怎么如实回答。
事实(来自 git 历史):项目的 16 个提交集中在 2026-09-11 到 09-13,提交信息里都带有 AI 编码工具的共同作者标记。
建议的回答方向:
开发时我用了 Claude Code 这类 AI 编码工具,git 提交记录里也能看到。代码主体是在几天里迭代出来的。
我自己负责的是这些判断:任务怎么定义、成功标准怎么定、评测集怎么分档、为什么要多轮和留出集、每一轮结果说明了什么、哪些结论不能下。比如”90% 到 100% 不能归因于知识库”、发现验收判错、决定作废 45 次结果重跑,这些都是看数据和轨迹之后做的决定。
代码我都能讲清楚,您可以挑任意一段问。
要做的准备:既然这样回答,就必须真的能讲清每一段代码。本系列 04、05、07、10、11、12、14 篇逐段讲过项目的工具层、循环、检查点、记录、安全检查、LangGraph 版本、MCP server 和后端,面试前至少把这些段落对着源码过一遍。
不要做的:不要否认用了 AI 工具,git 历史是公开的;也不要把”用 AI 写代码”说成主要能力。
4.7 容易踩的表达坑
| 坑 | 例子 | 改成 |
|---|---|---|
| 夸大因果 | ”加了知识库效果提升了" | "没有观察到可测量的差异” |
| 数字不说算法 | ”平均 13 步" | "v3 baseline 10 个项目 3 轮、只算通过的,平均 13.6 步” |
| 技术名词堆砌 | ”用了 LangGraph、MCP、RAG、RRF、Milvus” | 每个说清楚解决了什么问题、结果如何 |
| 把没做的说成做了 | ”有沙箱" | "工具层检查,明确不是沙箱” |
| 回避失败 | 只讲最终 60/60 | 先讲验收 bug 和重跑,60/60 反而说明评测集要加难度 |
| 讲太细停不下来 | 一开口就讲 ELF 解析 | 先讲结构,面试官感兴趣再展开 |
| 背稿痕迹重 | 一字不差念 | 记住要点和数字,用自己的话讲 |
第五部分 对照项目
本篇的”对照”是把系列里每篇的知识点,对应到项目讲稿里的哪一段:
| 系列篇目 | 讲稿里的位置 | 面试时能说的项目证据 |
|---|---|---|
| 01 大模型基础 | 成本 | 输入是输出的 41.6 倍,每步重发历史 |
| 02 模型 API 工程 | 局限 | 402 余额不足崩溃;没有告警 |
| 03 Prompt 工程 | 工具设计 | 知识库工具描述改写后调用从 3 次到 5 次,仍然低 |
| 04 工具调用与工具设计 | 十分钟版 ② | 分页读、截断保留退出码、错误信息引导 |
| 05 Agent 循环 | 十分钟版 ② | 步数上限、卡住检测、写文件后清零 |
| 06 上下文工程 | 工具设计 | 读文件分页、输出截断;41.6 倍 |
| 07 状态持久化 | 十分钟版 ⑦ | run 217 续跑;节点粒度 |
| 08 RAG | 十分钟版 ⑤⑥ | 48 条四类、45/48、覆盖是瓶颈 |
| 09 评测 | 十分钟版 ③④⑥ | 两个验收 bug、三条证据、留出集、一倍标准差规则 |
| 10 可观测 | 十分钟版 ⑨ | runs.db 两张表;没记缓存命中 |
| 11 安全 | 十分钟版 ⑧ | cat 绕过、不是沙箱、环境变量 |
| 12 框架与协议 | 十分钟版 ⑦ | 65 vs 126 行;MCP server |
| 13 多 Agent | 追问 | 为什么没用:串行依赖、共享目录 |
| 14 AI 应用后端 | 十分钟版 ⑨ | SSE、锁、409、断开放锁的 bug |
| 15 系统设计题 | 追问 | 按八步框架讲项目 |
第六部分 追问清单
30 个高频追问,按主题分组。回答方向里的数字都在第一部分数字卡片里。
| # | 追问 | 回答方向 |
|---|---|---|
| 1 | 为什么选交叉编译这个题目 | 工作经验相关,能判断对错;结果可程序验证 |
| 2 | 为什么用 Agent 不用脚本 | 路径不固定,下一步依赖报错 |
| 3 | 用的什么模型,为什么 | deepseek-flash,便宜,评测要跑几百次;没做多模型对比,是局限 |
| 4 | 成功标准怎么定的 | 程序重新构建 + 目标架构 ELF 产物;讲演变 |
| 5 | 验收会错吗 | 两个 bug、怎么发现、回归测试 |
| 6 | 模型会不会钻验收空子 | run 44 给 header-only 项目加示例目标 |
| 7 | 卡住怎么判断 | 重复调用计数;写文件后清零 |
| 8 | 步数上限怎么定的 | 25 → 40 的单变量实验;成本权衡 |
| 9 | 工具设计改过什么 | 读文件分页、截断保留退出码、toolchain 环境变量 |
| 10 | 知识库怎么建的 | 运行记录 → 分组 → 模型候选 → 人工审核 |
| 11 | 为什么检索测试集分四类 | 原始报错对 BM25 有利;改写和变体才测得出差异 |
| 12 | RRF 怎么算 | 各路排名倒数加 k 求和,k=60 |
| 13 | 为什么不用重排 | 总分一样、慢 30 多倍 |
| 14 | 知识库到底有没有用 | 未能证明;三条证据;留出集无差异;瓶颈在覆盖和调用率 |
| 15 | 那 RAG 做得有什么意义 | 方法跑通、离线和端到端分层、定位瓶颈 |
| 16 | 为什么要留出集 | 防止提前看到答案 |
| 17 | 每个配置跑几轮?够吗 | 3 轮;不够,只能说趋势;一倍标准差规则 |
| 18 | 结果有统计显著性吗 | 没做正式检验;配对重抽样区间跨过 0(09 篇实验) |
| 19 | 60/60 说明什么 | 评测集饱和,要加难度 |
| 20 | LangGraph 和手写有什么区别 | 结果一样;代码量、控制流、检查点 |
| 21 | 续跑会重复执行工具吗 | 节点粒度;需要幂等 |
| 22 | 踩过 LangGraph 什么坑 | 线程和 SQLite、递归上限默认值、invalid_tool_calls |
| 23 | MCP server 做了什么 | 复用工具层;越界返回 isError |
| 24 | 安全上发现过什么问题 | cat 绕过;路径前缀;.. 项目名 |
| 25 | 你的 Agent 安全吗 | 拦得住什么、拦不住什么、应该靠容器 |
| 26 | 后端怎么设计的 | 线程 + 队列 + SSE + 锁;断开放锁 bug |
| 27 | 成本多少 | 288 次输入 4870 万 token;没记缓存命中,只能粗估 |
| 28 | 如果上线要改什么 | 容器、任务队列、多用户、告警、心跳 |
| 29 | 最难的是什么 | 判断结果说明什么;评分器会错 |
| 30 | 代码是你写的吗、做了多久 | 如实说用了 AI 编码工具、几天迭代;讲自己的判断;能讲清每段代码 |
第七部分 闭卷自测
1. 不看稿,说出一分钟版的三个要点。
答案
一是项目是什么、为什么做(交叉编译 Agent;工作中做过二十多款工具的 ARM64 适配);二是看重评测(程序独立验收、10+5 个项目、每配置三轮、比较了知识库、检索方式、框架);三是有价值的发现(验收自身错过两次;90% 到 100% 不能归因于知识库,留出集也无可测量收益)。
2. “90% → 100% 不能归因于知识库”的三条证据是什么?
答案
步数变化方向混杂(4 个变好、4 个变差、2 个持平);两百多次工具调用只查了 3 次知识库;被救回的 re2 查的内容当时库里没有。另外每个配置只跑了 1 轮。
3. 两个验收 bug 分别是什么、怎么发现的、影响是什么?
答案
一:只看 cmake --build 退出码,json 两次空构建判通过;复查复验输出为空发现;旧数字作废,按新标准重跑。二:验收写死 build/ 目录,libarchive 仓库自带 build/ 源码目录,Agent 换目录名编译成功却判失败;9 次失败 8 次、报错都是 could not load cache 发现;45 次结果作废重跑。
4. 为什么 toolchain 改环境变量这个改动”因果清楚”,而困难档步数下降”只能算趋势”?
答案
v2 到 v3 只改了这一处,“子目录找不到 toolchain”从 8 次到 0、误拦消失,在轨迹里能直接对应到改动。困难档步数每格只有 6 次,标准差 5 到 8 步,差距不到一倍标准差,也没法在轨迹里直接对应。
5. 实验里第一版 baseline 平均步数算出来是 11.8,REPORT 写 13.1,谁错了?
答案
都没错,算法不同。REPORT 把 10 个项目全算上,包括失败时跑满 25 步的 re2;脚本只算通过的 9 个。讲数字时必须说清算法。
6. 为什么要做留出集?留出集上发现了什么?
答案
新增经验是从原 10 个项目的运行记录提炼的,再用它们测等于提前给了答案。留出集上:又发现构建目录写死的验收 bug;修复后三种配置全过,步数差异在一倍标准差内;30 次开知识库只调 8 次检索,查得最多的 libarchive 链接错误库里没有,瓶颈是覆盖不是算法。
7. LangGraph 版和手写版的对比结论是什么?续跑实验证明了什么、没证明什么?
答案
结果无差别(30/30 vs 29/29,另 1 次 402 不计),代码 65 vs 126 行,LangGraph 多出的是检查点和续跑。续跑证明了检查点之间的状态能正确恢复、已完成的工具调用不重复;没证明节点执行中途被杀也不重复,按机制分析是会重复的。
8. 说出至少五个项目的局限,并用”原因—影响—如果继续做”的句式讲其中一个。
答案
评测集饱和、样本少、知识库无可测量收益、不是沙箱、子进程能读 API key、没记缓存命中和模型耗时、没有告警、SSE 无心跳、只能单用户、只跑了一个模型。例:评测集饱和,原因是 10 个项目里 8 个关掉测试就能过,影响是通过率无区分度只能比步数和 token,如果继续做会加依赖链更深、非 CMake、需要 try_run 的项目。
9. 简历上”混合检索 Recall@1 45/48 最高,重排同分但慢 30 余倍,未采用”会被怎么追问?
答案
为什么测试集分类(原始报错对 BM25 有利,改写和变体才测得出差异);RRF 怎么算(按排名倒数融合,k=60);为什么不用重排(总分一样,33ms 变 1126ms);那端到端效果呢(留出集无可测量收益,离线分数高估实际作用)。
10. 被问”代码是你自己写的吗”,怎么如实回答?
答案
如实说用了 AI 编码工具(git 历史可见)、几天迭代完成;重点讲自己负责的判断:任务和成功标准定义、评测设计、多轮和留出集、结果解读、推翻错误结论、决定作废重跑;表示能讲清任意一段代码,并确实提前准备好。
延伸阅读
- 项目仓库:github.com/0612like/crossbuild-agent —
README.md、eval/REPORT.md - 00 学习路线与面试地图 — 回到系列目录,按面试重要度复习
- 09 评测 — 项目评测全过程时间线
- 15 系统设计题 — 用八步框架讲项目
这是系列的最后一篇。回到 00 学习路线与面试地图,按面试前的复习顺序过一遍。