Skip to content

00 导读:项目速览与「简历 ↔ 代码」事实校正 ​

本文是整个面试备赛包的地基。先读这一篇,把数字和事实钉死,后面 7 篇才不会被问崩。


一、这份备赛包包含什么 ​

文档作用建议用法
00-导读-项目速览与事实校正.md项目定位 + 可背诵数字 + 简历与代码不一致的地方第一遍精读,之后每天扫一遍校正表
01-项目口述稿-多版本.md30 秒 / 1 分钟 / 3 分钟 / 8 分钟口述稿大声朗读,掐表练
02-简历五大技术点拆解.md简历 5 条逐条拆成「口述 + 原理 + 代码依据 + 备注」主力背诵材料
03-面试题库-协议与Agent循环.md多协议抽象 + ReAct 循环深挖题面试前一天刷
04-面试题库-权限与安全.md五层权限深挖题(字节最爱的安全题)重点刷,容易被追 3 层以上
05-面试题库-MCP与会话压缩.mdMCP + 上下文压缩 + 持久化深挖题重点刷
06-压力面-陷阱题与简历修正.md简历被质疑、你不会的题、主动暴露的缺陷一定要看,防翻车
07-白板手写题与反问收尾.md可能的现场编码题 + 反问清单面试当天早上看

每道题的格式固定为四段:

  1. 面试官想听什么 —— 他真正在验证的能力点
  2. 口述答案(可直接背) —— 用大白话讲,控制在 30–90 秒
  3. 备注讲解 —— 为什么是这个答案、代码在哪、数字多少
  4. 可能的追问 —— 他会怎么往下挖

二、项目一句话定位 ​

MewCode / EasyCoding 是一个用 Go 从零手写的终端 AI 编程助手(对标 Claude Code):单二进制、Anthropic + OpenAI 双协议、多轮 ReAct 闭环、五层权限防御、MCP 工具生态、两层上下文压缩、JSONL 会话存档 + 流式 TUI。

面试开场用它,一句话说清「是什么 / 多难 / 有什么」。


三、可背诵的「事实底牌」 ​

这些数字请务必背下来,面试时随口报出来,可信度立刻上一个台阶。

项目数值出处
语言 / 模块Go 1.25.8,module mewcodemewcode/go.mod
代码规模101 个非测试 Go 文件 / 14,082 行;17 个测试文件 / 3,902 行;合计 17,984 行find + wc -l
包结构18 个 internal 包 + cmd/mewcode(主程序)+ cmd/smoke(冒烟)mewcode/internal/
内置工具6 个:read_file / write_file / edit_file / bash / glob / greptool.NewDefaultRegistry()
扩展工具LoadSkill(只读、系统工具)/ InstallSkill(有副作用);Task 四工具;Agent 工具tui/tui.go 注册
工具执行超时Agent 层统一 30s(tool.DefaultTimeout);bash 自身默认 120stool/registry.go:13、tool/bash.go
bash 输出截断200 行 / 8000 字节tool/bash.go:116
bash 环境变量白名单仅 4 个:HOME / PATH / USER / TERM(不继承父进程环境)tool/bash.go
read_file 截断2000 行 / 256KBtool/read_file.go
glob / grep 结果上限各 100 条,超出追加 [truncated]tool/glob.go、tool/grep.go
Agent 迭代上限25 轮(maxIterations)agent/agent.go:150
幻觉熔断连续 3 轮整轮只请求未注册工具即停(maxUnknownRun)agent/agent.go:151
Plan 提醒节奏每 4 轮一次完整版(第 1/5/9/13/17/21/25 轮),其余轮次精简版agent/agent.go:152
权限模式4 档:Default / AcceptEdits / Plan / BypassPermissionspermission/mode.go
危险命令黑名单10 条正则,不可配置放开,Bypass 也拦permission/blacklist.go
权限规则三层local(settings.local.yaml)> project(settings.yaml)> user(~/.mewcode/settings.yaml)permission/engine.go
规则匹配语法=精确 / ~正则 / !取反 / 缺省 glob;层内 deny 优先于 allowpermission/matcher.go、rule.go
MCP 命名空间mcp__<server>__<tool>(双下划线)mcp/tool.go:adaptTool
MCP 连接超时30s / server,并发连接,失败只跳过自己mcp/manager.go
MCP 工具名限制仅允许 [A-Za-z0-9_-],否则该工具被跳过mcp/tool.go
压缩触发阈值ContextWindow − 20000(SummaryReserve)− 13000(AutoSafetyMargin)compact/const.go
压缩层 1 阈值单条工具结果 > 50,000 字节 或单条 tool 消息聚合 > 200,000 字节 → 落盘替换compact/const.go
压缩层 2 熔断连续 3 次自动摘要失败即跳闸compact/const.go
摘要 PTL 重试前 3 次每次丢最旧 1 组,之后按 20% 比例丢compact/layer2.go
保留近期原文≥ 10,000 token 且 ≥ 5 条消息(两个下界都满足才停)compact/const.go
恢复段最近 5 个文件 × 每文件 5,000 tokencompact/const.go
Token 估算系数3.5 字符 / tokencompact/const.go
系统提示模块固定 7 个(优先级 10–70)+ 可选 3 个(80 / 90 / 100)prompt/modules.go
会话清理启动时 goroutine 后台清理 30 天以上会话cmd/mewcode/main.go:121
会话文件<root>/.mewcode/sessions/<YYYYMMDD-HHMMSS-xxxx>/conversation.jsonl,每次写后 file.Sync()session/writer.go
工具结果落盘目录<sessionDir>/tool-results/<tool_use_id>compact/state.go
git status 采集超时2s,失败降级留空prompt/environment.go
测试go test ./... 全绿;11 个包有测试,7 个包无测试文件见下文「软肋」

四、⚠️ 简历 ↔ 代码 事实校正表(最重要的一节) ​

你在简历上写的描述不是全部与代码一致。字节的面试官会当场要求你打开源码,或者让你口述细节。下面 6 处不一致必须提前改口,否则被抓住就是「简历注水」,性质比「不会」严重得多。

校正 1:迭代上限不是 10 轮,是 25 轮 🔴 必须改 ​

  • 简历写:「完整思考→行动→观察→思考循环(10 轮迭代上限)」

  • 代码真相:mewcode/internal/agent/agent.go:150 → maxIterations = 25;停止提示文案也写死了「已达最大迭代轮数 25,自动停止;可继续发消息推进。」(agent.go:157)

  • 为什么会有这个偏差:项目 README.md 里写的是「最多 10 轮迭代」,那是早期版本的陈述,代码后来调到 25 但 README 没同步。你写简历时抄了 README。

  • 怎么改口:

    「迭代上限是 25 轮,不是 10 轮——这是个硬编码常量 maxIterations,连停止提示文案里都写死了 25。我简历上写的 10 轮是早期版本的数字,后来调高了,README 没同步,这里以代码为准。」

    主动纠正比被抓住强 10 倍。 这句话还顺手展示了「以代码为准」的工程态度。

校正 2:MCP 命名空间分隔符是双下划线 🟡 建议改 ​

  • 简历写:mcp<server><tool>
  • 代码真相:mcp/tool.go → fullName := "mcp__" + serverName + "__" + t.Name,即 mcp__github__create_issue
  • 为什么重要:单下划线在 server 名或 tool 名本身含下划线时会产生歧义(server my_server + tool get_x → mymy_serverget_x),双下划线是刻意的消歧设计。这其实是个加分点,讲出来比写对更有价值。

校正 3:「连续幻觉检测」的精确语义 🟡 建议说准 ​

  • 简历写:「连续幻觉检测自动停止」
  • 代码真相:判定函数是 allUnknown(registry, calls) —— 一整轮里模型请求的每一个工具名在 Registry 里都查不到,才计一次 unknownRun++;只要有一个命中就把计数器清零。连续 3 轮这样的全部落空 → 停止。
  • 口径修正:「连续 3 轮、且每轮全部工具调用都是未注册的工具名才停——中途只要它试对了一次,计数就清零重来。」

校正 4:「五层权限防御」的层归属要说清楚 🟡 建议说准 ​

  • 严格讲:permission.Engine.Check() 只实现前四层(黑名单 → 沙箱 → 规则引擎 → 模式兜底),第五层「人在回路」由 agent 包编排(agent.go 拿到 permission.Ask 后 emit(Event{Approval: ...}) 给 TUI,然后阻塞在 Respond channel 上等用户回传)。
  • 口径:这不影响「五层防御」成立,但如果你把「引擎负责判定,Agent 负责编排审批」这条边界讲清楚,面试官会觉得你真读过代码。
  • 代码引用:permission/mode.go 的包注释自己就写了这句话——「前四层(黑名单 → 沙箱 → 规则引擎 → 模式兜底)由 Engine.Check 实现,第五层(人在回路)由 agent 包编排驱动」。照着包注释说,完全站得住。

校正 5:「从零手写」要说清楚「零」在哪 🔴 必须准备 ​

  • 项目里的第三方 SDK:anthropic-sdk-go、openai-go/v3、modelcontextprotocol/go-sdk、bubbletea/v2 + bubbles + lipgloss + glamour
  • 面试官会问:「你这全是 SDK,也叫从零手写?」
  • 标准答法(务必背):

    「『从零手写』指的是不依赖 Agent 框架——没用 LangChain、没用 Eino、没用任何 ReAct / Chain / Memory 框架,Agent 的运行时(循环、工具编排、权限、压缩、会话)100% 是自己写的。 我在三个地方用了 SDK,而且是有意选择的: ① LLM 协议层用官方 SDK——流式 SSE 解析和 tool_use 增量拼接是纯苦力活,自己写只会引入 bug;而且这块正好是我要抽象掉的,SDK 藏在 llm.Provider 接口后面; ② MCP 用官方 go-sdk——协议细节不该重造; ③ TUI 用 Bubbletea——Elm 架构,我只写 Model / Update / View。 一句话:入口层(协议、传输、渲染)用成熟库,核心层(Agent 循环、权限、上下文)全手写。这是我认为正确的工程判断。」

校正 6:「30 天清理」和「实时 fsync」都是真的 ✅ 可以放心讲 ​

  • main.go:121 → go func(){ session.CleanExpired(sessionsDir, 30*24*time.Hour) }(),确实在 goroutine 后台跑。
  • session/writer.go 的 Append 和 WriteCompactMarker 结尾都调了 w.file.Sync()——Go 的 File.Sync() 就是 fsync(2),说「fsync」没错。

五、⚠️ 项目软肋(面试官一定能查到,必须提前准备答案) ​

这一节是你主动要抛出去的东西。面试官挖到弱点时,如果你能抢答,效果是「这个人很清醒」;如果被挖出来你才承认,效果是「这个人没做过 code review」。

软肋 1:7 个包没有测试文件 🔴 最危险 ​

执行 go test ./...,以下包显示 [no test files]:

mewcode/internal/compact      ← 上下文压缩(简历重点吹的能力)
mewcode/internal/llm          ← 协议抽象(简历第 1 条)
mewcode/internal/session      ← 会话持久化(简历第 5 条)
mewcode/internal/subagent
mewcode/internal/task
mewcode/internal/memory
mewcode/internal/instructions

有测试的是:agent、command、config、conversation、hook、mcp、permission、prompt、skills、tool、cmd/mewcode。

问题:简历第 1 条(协议)和第 5 条(会话持久化)恰好是零测试的模块。

准备答法:

「测试覆盖是不均匀的,我得诚实说:permission、agent、tool、mcp、hook 这几个包测试比较扎实,但 compact、session、llm 这三个没有单元测试——恰恰是我简历上重点讲的两块。 原因是这三个模块要么强依赖真实 LLM 往返(压缩要发摘要请求),要么强依赖文件系统和时间。我当时判断用 tmux 端到端手工验收性价比更高,就没补单测。 现在回头看这是个错误的取舍。 compact 里的 OffloadAndSnip、pickRecentTail、EstimateTokens 都是纯函数,完全可以穷举单测;session 的 JSONL 读写也是纯 IO,用 t.TempDir() 就能覆盖。 如果继续做,我会按『纯函数优先』补:先补 compact/state.go 的替换决策账本和 layer1.go 的落盘阈值判定,再补 session 的坏行跳过和孤立工具调用截断。」

这段回答的三个得分点:① 坦诚;② 说清了为什么会这样(不是懒,是取舍失误);③ 给出了具体、可执行的补救方案(证明你清楚这些模块的测试点在哪)。

软肋 2:bash 工具能绕过文件沙箱 🔴 权限题必被挖 ​

  • 真相:第二层沙箱只对文件类工具(read_file / write_file / edit_file / glob / grep)生效,判定依据是 extractTarget 解析出的路径参数。bash 属于 CategoryExec,沙箱根本不看它的命令串。

  • 后果:bash 里执行 cat /etc/passwd 或 cd /tmp && ... 时,沙箱不拦。

  • 现在靠什么兜:① 黑名单正则(只覆盖 10 种极端破坏模式,覆盖不到 cat /etc/passwd);② 权限模式兜底——bash 在 default / acceptEdits / plan 下都判 Ask,所以默认会弹窗让人确认。真实风险被「默认 Ask」挡住了,但如果用户配了 Bash 全放行规则、或切到 Bypass 模式,沙箱就真的失守了。

  • 准备答法:

    「这是个真问题。我的沙箱是参数级的,不是进程级的:它只解析文件类工具的 path 参数,对 bash 完全不生效。 现在的防线是『bash 在 default / acceptEdits / plan 下都判 Ask』,所以默认有弹窗兜底;但一旦用户写了 Bash 全放行规则或切 Bypass,就只剩黑名单那 10 条正则,而黑名单只覆盖 rm -rf /、mkfs、dd of=/dev/* 这类极端破坏,cat /etc/passwd 是拦不住的。 正确的解法是把沙箱下沉到进程级:Linux 上用 seccomp / landlock 或 bubblewrap 做 mount namespace 隔离,macOS 上用 sandbox-exec 的 seatbelt profile,把 bash 子进程的文件系统视图直接限制在项目根。 退一步的过渡方案是命令静态分析:解析 sh -c 的命令串,抽出所有绝对路径参数和 cd 目标做白名单校验——但那本质是个不完备的启发式规则,我不认为它能替代真正的 sandbox。」

    补充事实:tool/bash.go 里已经有一个很小的补丁——workdir 参数会拒绝绝对路径和 ..;但这只堵了一个小洞,不改变上面的结论。

软肋 3:mcp__server__* 通配规则实际上是失效的 🔴 你可能不知道这个 bug ​

我在项目源码上跑了真实探针验证过(在 permission 包内写临时测试,直接调 parseRule / RuleSet.match / escapeGlob,输出如下):

[1] parseRule("mcp__github__create_issue") → Tool="mcp__github__create_issue" Matcher==nil? true
    matchRule(r1, "") = true                                  ← 精确名规则可用
[2] parseRule("mcp__github__*") → Matcher==nil? true
    matchRule(r2, "") = true                                  ← 但 Tool 名是字面量 "mcp__github__*"
[3] parseRule("mcp__github__create_issue(...)") → matchRule(r3, "") = false
[6] 规则 mcp__github__create_issue 对 (friendly=全名, target="") → hit=true  decision=Allow
[7] 规则 mcp__github__*            对 (friendly=全名, target="") → hit=false ← 失效
[8] extractTarget(MCP 工具) → isFile=false ok=false           ← 所以 target 恒为空串
[9] categorize("mcp__github__create_issue", readOnly=false) = CategoryExec? true

根因(有两层,比想象的更绕):

  1. extractTarget() 只对六个内置工具有分支(read_file / write_file / edit_file / glob / grep / bash),default 分支直接返回 ("", false, false)。所以 MCP 工具的 target 恒为空串。
  2. 规则语法不允许通配「工具名」。parseRule 的格式是 Tool(pattern)——括号里那部分只用来匹配参数,而 Tool 那部分必须与 friendly 名完全相等。 于是 mcp__github__* 因为没有括号,被整体解析成「一个名叫 mcp__github__* 的工具」(pattern="" → Matcher=nil),而 r.Tool == friendly 比较时 "mcp__github__*" != "mcp__github__create_issue" → 永远不命中。 就算写成带括号的 mcp__github__(*),Tool 会变成 "mcp__github__",同样不等于工具全名 → 还是不命中。

后果:README 里宣传的 mcp__github__* 通配放行实际不生效。用户只能写不带括号的工具全名:

yaml
permissions:
  allow:
    - "mcp__github__create_issue"    # ✅ 生效:Matcher 为 nil → matchRule 恒返回 true
    - "mcp__github__*"               # ❌ 不生效:被当成一个字面工具名

这意味着接入一个 20 个工具的 MCP server,用户要写 20 条规则 —— 可用性问题很严重。

这个问题怎么用:

  • 如果你主动说出来,这是全场最强的加分项之一——说明你不是「Vibe 完就完了」,你真的回归验证过行为边界。
  • 答法:

    「我后来回归测试时发现一个 bug:MCP 工具的权限规则通配是失效的。我去读了规则解析的代码才搞清楚根因,有两层: 第一层是 extractTarget 没有 MCP 工具的分支,所以 MCP 工具调用的 target 恒为空串——而规则语法里括号那部分是拿来匹配参数的; 第二层更隐蔽:规则格式是 Tool(pattern),Tool 那部分必须和工具名完全相等,不支持通配。所以 mcp__github__* 因为没带括号,被整体当成了「一个名叫 mcp__github__* 的工具」,而实际工具名是 mcp__github__create_issue,比较永远不相等。 结论是用户只能写不带括号的工具全名才能放行,而接入一个 20 个工具的 server 就要写 20 条规则——可用性问题很严重。 修法有两种:一是让规则匹配支持「工具名 + 参数」两级(工具名支持通配、参数也支持通配),这样 mcp__github__* 和 Bash(git *) 就能共用一套语法;二是给 extractTarget 补 MCP 分支把它当参数——但我觉得第二种治不了本,因为工具名和参数是两个不同维度。 这个 bug 现在还在,我没修——因为它不影响安全性(匹配不上就不放行,继续弹窗,是 fail-safe 的),只影响可用性。」

注意:一定要强调它是 fail-safe 的(匹配不上 = 不放行 = 继续 Ask),不构成安全漏洞,否则面试官会以为你留了个安全洞。

软肋 3.5:后台 Agent 工具白名单里的 Skill 工具名拼错了 🟡 附带发现 ​

  • tool/filter.go 的 ASYNC_AGENT_ALLOWED_TOOLS 写的是小写 "load_skill", "install_skill";
  • 但两个工具的 Name() 实际返回的是首字母大写的 "LoadSkill" / "InstallSkill"。
  • 后果:后台(异步)子 Agent 的 Skill 工具白名单形同虚设——交集算下来是空的,Skill 工具在后台 Agent 里不可用。
  • 答法:和上面那个 bug 一起说,作为「我知道我的工具过滤层有两个命名不一致问题」的证据。修法很简单:统一工具命名规范(要么全小写下划线,要么全大驼峰),或者把白名单改成引用 Tool.Name() 常量而不是字符串字面量。

软肋 4:权限检查与工具执行之间存在 TOCTOU 窗口 🟡 ​

  • 真相:沙箱在 Engine.Check 时用 EvalSymlinks 解析路径、再和 root 做前缀比对;但工具真正 os.Open 是几百微秒之后的事。理论上攻击者可以在两者之间把软链接换掉。
  • 准备答法:

    「存在一个 TOCTOU 窗口:我在权限检查时解析符号链接、比对前缀,但真正 open 是之后的事,中间可以被换链接。 在单用户本地工具这个场景下我不认为它是现实威胁——攻击者要能在你机器上改符号链接,他早就不需要绕你的沙箱了。但要上多租户服务端就必须修:标准做法是检查后持有 fd(openat + O_NOFOLLOW,或者 Linux 上先 open 再用 fstat 校验 inode 是否仍在 root 下),把『校验』和『使用』合并成一次原子操作。」

软肋 5:compact 的 Token 估算是启发式的 🟡 ​

  • 真相:EstimateTokens = 上次真实 usage 之和 + 增量字符数 / 3.5(estimateCharsPerToken = 3.5)。3.5 是经验系数,中英文混合时会偏。
  • 兜底机制:① 自动触发留了 13,000 token 的安全余量(AutoSafetyMargin),就是为了吃掉估算误差;② 如果真估错、请求被拒,会拿到 ErrPromptTooLong 然后走紧急压缩(TriggerEmergency,只检查 3,000 余量)并重发一次。
  • 准备答法:

    「是启发式估算,系数 3.5 字符/token 是经验值,中文场景会低估。我不打算把它做成精确分词器——那要给每种模型捆绑一个 tokenizer,模型换代就得跟着升级。 我的设计是用余量吃误差 + 用错误兜底:自动阈值留了 13,000 token 的 AutoSafetyMargin;真估错了被 provider 拒了,会捕获 ErrPromptTooLong 立刻走紧急压缩再重发一次,用户无感。 关键设计是『锚点 + 增量』:锚点用上一次 provider 返回的真实 usage(input + output + cache_read + cache_write),只有锚点之后的新增消息才靠字符数估算。所以误差不会累积,每一轮都会被真实值重新校准。」

软肋 6:session 恢复后并非立刻压缩 🟡 ​

简历写「/resume 浏览历史会话,Token 超限自动压缩」。真相是:恢复后不是立刻压缩,而是等下一轮 Agent Loop 开始时走正常的自动压缩判定(Run 第一轮就会调 ManageContext)。效果等价,但如果被问「恢复的时候会不会卡」,要能答上来。

软肋 7:Conversation 的两个 Add 方法按引用存切片 🟡 并发细节 ​

  • AddAssistantWithToolCalls(text, calls) 和 AddToolResults(results) 直接把入参切片存进内部(没有 copy);而 Messages()、ReplaceMessages()、NewFromMessages() 都是深拷贝。
  • 风险:调用方如果复用/修改这个切片,会污染对话历史。当前代码里没问题(每次 executeBatched 都新建 results 切片),但这是一颗定时炸弹。
  • 答法:

    「Conversation 的读接口全是深拷贝,但两个写接口是引用语义,这是不一致的。当前没有触发 bug,因为调用方每次都新建切片,但这是隐患。修法就是在这两个方法里也补 copy。」


六、面试时间分配建议(按 45 分钟一场算) ​

阶段时长讲什么
自我介绍 + 项目开场3–5 min用 01-口述稿 的 3 分钟版
面试官挑一个点深挖15–20 min大概率是权限(安全题)或 ReAct 循环
继续深挖第二、第三个点10–15 min大概率是 MCP 或 上下文压缩
手写代码 / 白板5–10 min见 07,大概率是「写个权限匹配器」或「写个并发保序」
反问5 min见 07

如果只有时间准备两篇:04-面试题库-权限与安全.md + 06-压力面-陷阱题与简历修正.md。


七、开始前最后一句提醒 ​

这个项目的技术含量是够的——18 个包、1.8 万行、双协议 + MCP + 五层权限 + 两层压缩,这个组合在应届/实习简历里属于前 5%。

你唯一的风险不是「项目不够好」,而是「说不清」。

所以接下来 7 篇文档的每一个「口述答案」,都请你出声读三遍,而不是默读。默读会骗你——你以为你会了,一张嘴就卡壳。


➡️ 下一篇:01-项目口述稿-多版本.md

持续学习,持续构建。