我构建了什么
grill是一个模拟技术面试官,它读过你的代码。它运行在终端里,基于一个开放模型,在你自己的笔记本电脑上。
我为我的朋友Akash构建了它。他过去一年一直在写 Rust,副业发布 TypeScript,正在寻找一份全职工作。他最害怕的面试环节是 Rust 和 JavaScript 的深度追问。
大多数面试准备工具问的都是教科书式的问题:什么是闭包,解释一下所有权。真正的高级面试不是那样进行的。它们会说“带我过一遍你的项目”,然后面试官会指着你希望没人注意到的那一行,问为什么。
你无法用题库来练习这个,因为问题取决于你的代码。所以grill做了三件事:
-
读取他的代码仓库并找出值得提问的代码行:
unsafe代码块、锁、生命周期、派生的任务、useEffect,forEach(async ...),any,空的catch。 - 就这些内容对他进行面试。一个关于那段确切代码的问题,一个针对他回答中最薄弱部分的追问,然后给出 0 到 4 的评分以及他遗漏了什么。
- 记住他答错的内容。下一次会话会更多地从他最差的话题中出题。
下面是扫描器在 Akash 四个公开代码仓库上的运行结果。这是真实输出:
116 处,最靠前的一处在miccli,他的语音听写 CLI:一个Arc<Mutex<...>>与实时音频回调共享。这正是 Rust 面试官会圈出来的那一行。
演示
一次会话是这样的。代码和位置是真实的扫描器输出;把占位行替换成你自己在真实运行中的问题、回答和评分:
── 1 / 5 ──
AkashJana18/miccli src/audio.rs:62 共享-状态
62 │ pub fn start_capture(&self) -> Result<AudioStream> {
63 │ let (tx, rx) = mpsc::channel();
│ ...
72 │ let sink = Arc::new(Mutex::new(PendingResampler::new(
│ ...
代码
拷问
一个读过你代码的模拟面试官。它在你笔记本电脑上运行,基于一个开放模型,无需网络。
大多数面试准备问的是教科书式的问题。真正的高级面试会问“带我过一遍你的项目”,然后追问你希望没人注意到的那一行。grill克隆你的代码仓库,找到那些行,并针对它们提问。
为Akash构建,他写 Rust 和 TypeScript,正在面试全职岗位。
── 1 of 5 ──
AkashJana18/miccli src/audio.rs:62 shared-state
62 │ pub fn start_capture(&self) -> Result<AudioStream> {
63 │ let (tx, rx) = mpsc::channel();
│ ...
72 │ let sink = Arc::new(Mutex::new(PendingResampler::new(
│ ...
Why is `sink` behind a Mutex, and what does the audio callback do if that lock is contended?
>
上面的代码和位置是真实的扫描器输出。问题是模型会提出的那类问题的示例;你遇到的会有所不同。
演示
已录制…
约 1,300 行 TypeScript,包括测试,三个运行时依赖,MIT 许可。
ollama pull gemma3:4b
git clone https://github.com/yashksaini-coder/grill && cdgrill
npm install && npm run build && npm linkgrill ingest AkashJana18/miccli AkashJana18/solana-consensus-lab
grill start # 5 个问题
grill start -n 3 --lang rust # 仅 Rust
grill report # 薄弱点地图
我是如何构建它的
开放的部分:
设计规则:模型一次只做一件小事
一个在笔记本电脑上运行的 4B 模型没法规划一场面试、在脑子里记住评分标准,并记得它五轮之前问过什么。所以我没有要求它这么做。步骤的顺序存在于普通的 TypeScript 中。模型只被要求做三件事之一:写一个问题、写一个追问,或给一段交流打分。
每一次调用都会再次拿到真实的代码片段。模型永远不必回忆代码,所以它编造代码的空间更小。
阶段 1:找出值得提问的代码行
扫描器是一张包含 38 个模式的表,17 个用于 Rust,21 个用于 JavaScript 和 TypeScript。每个模式都带有一个主题、一个权重,以及一位优秀面试官会采取的角度:
rust("lock", "concurrency", /\.(lock|read|write)\(\)\s*(\.await|\.unwrap\(\)|\?/, 4,
"guard 存活多久,它是否跨 await 持有,以及一个被毒化或争用的锁会怎样"),
js("async-in-loop", "async", /\.(forEach|map|filter|reduce)\(\s*async\b/, 5,
"这里实际 await 的是什么,完成的顺序,以及错误如何浮现"),
一行上的正则命中不是一个问题。所以对于每一次命中,扫描器会向上走到所在的函数,匹配花括号找到它的结尾,并把整个函数框起来。权重按函数累加,重叠的框架会被丢弃,保留最强的那个,每个仓库贡献它最好的 40 个位置,按主题轮转,这样满是.unwrap()的代码库不会把整场面试变成错误处理。
这是正则和花括号计数,不是解析器。它找到有趣的代码。它不理解代码。理解是模型的工作。
阶段 2:Gemma 上的两个 Mastra 智能体
Mastra 通过其兼容 OpenAI 的端点与 Ollama 通信,因此没有 provider SDK,也没有 API key:
const model = { providerId: "local", modelId: config.modelId, url: config.url, apiKey: "local" };
this.interviewer = new Agent({ id: "interviewer", name: "interviewer", instructions: INTERVIEWER, model });
this.grader = new Agent({ id: "grader", name: "grader", instructions: GRADER, model });
interviewer被要求只问一个问题,从代码片段中说出一个标识符的名字,以便候选人知道它读过代码,并且绝不回答或提示。grader
是一个独立的智能体,拥有独立的提示词。它没有编写问题,因此它对答案的好坏没有利害关系。它返回 JSON:{"score"
:2,"verdict":"one sentence","missed":["specific point"],"better":"the answer you wanted"}小模型会把 JSON 包裹在散文和代码围栏中。与其信任回复,不如提取出第一个平衡的
,用 zod 验证它,如果失败,就把模型自己的糟糕回复展示给它,并再问一次。grill还有一个可选的{...}工具(
)。它让 interviewer 能够读取代码片段之外的内容,例如查看调用方或类型定义。它被限制在仓库内:解析到检出目录之外的路径会被拒绝。它需要一个具备工具调用能力的模型,因此默认关闭。read_source阶段 3:弱点地图grill start --tools每一轮都会以 JSON 形式保存在
下。下一次会话会以加权抽取的方式挑选弱点:
export~/.grill/sessionsfunction
priority ( spot:Hotspot, seen: Set< string>,stats: TopicStat[]): number{ const stat
= stats . find((s)=>s . topic===spot . topic);constweakness
= stat ? ( 4 -stat . mean)/4 : 0.5 ; constrepeat
= seen . has(spot.id)?0.15 : 1 ; // 取平方,因此第一次会话会从最丰富的代码开始,而不是填充内容。return
spot
. score**2 * ( 0.5 +weakness ) *repeat ; }在 lifetimes 上得 1 分,lifetimes 就会再次出现。得 4 分,
它就会淡出。已经问过的弱点很少出现,但并未被禁止,因为同一个东西被问两次,才能发现你是否真正学会了它。
在没有 LLM 的情况下测试 LLM 应用unsafe我不想让测试套件需要 GPU。测试会在一个随机本地端口上启动一个假的兼容 OpenAI 的服务器,并让真正的 Mastra 智能体指向它。这覆盖了一整轮、JSON 重试、
以及
I did not want the test suite to need a GPU. The tests start a fake OpenAI-compatible server on a random local port and point the real Mastra agents at it. That covers a full round, the JSON retry,:skipand:quit,以及包含路径逃逸拒绝的工具调用。18 个测试,几秒钟,无需模型。
为什么开放式创新很重要?
对这个项目来说,这不是偏好问题。封闭的 API 会在四个方面让它在 Akash 上成为更差的工具。
他那些结结巴巴的回答仍然属于他自己。面试练习只有在你愿意犯错时才有效。grill按设计存储他给出的每一个薄弱回答。那个文件位于~/.grill在他的磁盘上。如果使用托管 API,他所有不知道的内容的同样记录就会在别人的服务器上。
私有代码可以正常工作。 grill ingest接受本地路径。受 NDA 保护的工作代码、他无法推送到任何地方的仓库、他还没准备好展示的副业项目:所有这些都可以在不离开本机的情况下进行面试练习。一旦模型被拉取,本地路径会话完全不需要网络。
练习是免费的,所以他可以大量练习。每个问题三次模型调用,每次会话五个问题,需要多少次会话就多少次。在按量计费的 API 上,这笔账单会随着我恰恰想要鼓励的行为而增长。
他可以更换面试官。评分标准是src/interview/mastra-brain.ts中的一个字符串。如果他在为一家更看重系统设计而非语言琐事的公司做准备,他就编辑提示词。如果他换了更好的笔记本电脑,GRILL_MODEL就通过一个环境变量换入更大的模型。如果 Ollama 不合他的口味,GRILL_URL就指向 llama.cpp 或 LM Studio。工具中没有任何东西绑定到单一供应商。
开源权重让它私密且免费。开放框架让它可更改。这三者他都需要。
诚实地谈局限
- 扫描器无法区分故意的
unwrap()和粗心的。它会浮现候选者;有些很无聊。 - 问题质量取决于模型。更大的本地模型会提出更尖锐的问题。
- 评分是学习辅助,不是判决。读“遗漏”列表,而不是数字。
奖项类别
- 最佳 Gemma 使用奖。Gemma 通过 Ollama 在本地运行,完成全部三项工作:提问、追问和评分。整个设计采用固定编排、每次调用一个小任务,就是为了让一个小型开源权重模型足够用。
-
最佳 Mastra 使用奖。两个 Mastra 智能体运行在开放模型之上,外加一个沙箱化的
read_source工具,连接到本地 OpenAI 兼容端点,不使用任何供应商 SDK。





