这是为Hacktoberfest 周末挑战:为朋友而构建

我构建了什么

grill是一个模拟技术面试官,它读过你的代码。它运行在终端里,基于一个开放模型,在你自己的笔记本电脑上。

我为我的朋友Akash构建了它。他过去一年一直在写 Rust,副业发布 TypeScript,正在寻找一份全职工作。他最害怕的面试环节是 Rust 和 JavaScript 的深度追问。

大多数面试准备工具问的都是教科书式的问题:什么是闭包,解释一下所有权。真正的高级面试不是那样进行的。它们会说“带我过一遍你的项目”,然后面试官会指着你希望没人注意到的那一行,问为什么。

你无法用题库来练习这个,因为问题取决于你的代码。所以grill做了三件事:

  1. 读取他的代码仓库并找出值得提问的代码行:unsafe代码块、锁、生命周期、派生的任务、useEffect,forEach(async ...),any,空的catch。
  2. 就这些内容对他进行面试。一个关于那段确切代码的问题,一个针对他回答中最薄弱部分的追问,然后给出 0 到 4 的评分以及他遗漏了什么。
  3. 记住他答错的内容。下一次会话会更多地从他最差的话题中出题。

下面是扫描器在 Akash 四个公开代码仓库上的运行结果。这是真实输出:

Terminal output of grill ingest finding 116 interview-worthy spots across four repositories

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: a mock interviewer that has read your code

拷问

Sponsor

一个读过你代码的模拟面试官。它在你笔记本电脑上运行,基于一个开放模型,无需网络。

大多数面试准备问的是教科书式的问题。真正的高级面试会问“带我过一遍你的项目”,然后追问你希望没人注意到的那一行。grill克隆你的代码仓库,找到那些行,并针对它们提问。

为Akash构建,他写 Rust 和 TypeScript,正在面试全职岗位。

I built my friend an interviewer that read his code

── 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?
  >

上面的代码和位置是真实的扫描器输出。问题是模型会提出的那类问题的示例;你遇到的会有所不同。

演示

grill interviewing a real repository: ingest, spots, a two-round interview, the weak-spot map

已录制…



约 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                   # 薄弱点地图

我是如何构建它的

Architecture of grill: ingest, interview and remember stages, all running locally on Ollama, Gemma and Mastra

开放的部分:

  • Gemma,Google 的开源权重模型,由Ollama在localhost上提供服务。
  • Mastra,开源 TypeScript 智能体框架,用于这两个智能体和工具。

设计规则:模型一次只做一件小事

一个在笔记本电脑上运行的 4B 模型没法规划一场面试、在脑子里记住评分标准,并记得它五轮之前问过什么。所以我没有要求它这么做。步骤的顺序存在于普通的 TypeScript 中。模型只被要求做三件事之一:写一个问题、写一个追问,或给一段交流打分。

One interview round: scanner shows a snippet, interviewer asks, candidate answers, interviewer follows up, grader scores, store saves

每一次调用都会再次拿到真实的代码片段。模型永远不必回忆代码,所以它编造代码的空间更小。

阶段 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。