06 压力面:陷阱题、简历修正与「不会」的应对
⚠️ 这一篇是保命文档。前面几篇教你怎么讲得漂亮,这一篇教你怎么不被问崩。
字节的技术面有一类固定环节叫「简历深挖 + 压力测试」——面试官会故意质疑你的项目、质疑简历的真实性、问你不会的东西,观察你的反应。 他考的不是知识,是「你在压力下是否诚实、是否有自己的判断」。
目录
- 第一部分:简历被质疑的 6 个场景
- 第二部分:⚠️ 项目里「定义了但没接线」的功能(高级答法)
- 第三部分:其他可能被挖出的缺陷清单
- 第四部分:「我不会」的标准应对框架
- 第五部分:压力面模拟对话(含逐句台词)
- 第六部分:面试前 10 分钟自检清单
第一部分:简历被质疑的 6 个场景
场景 1:「你这个项目是不是 AI 写的?」
⚠️ 一定会被问。这是 2026 年面试的必考题。
错误答法(自毁型)
❌ 「不是,都是我自己写的。」 → 一旦被追问细节就露馅,定性为「不诚实」,直接出局。 ❌ 「是的,基本都是 AI 写的。」 → 自我否定,面试官会立刻降低兴趣。 ❌ 「AI 只是辅助,主要还是我。」 → 空话,没有证据,面试官会继续追。
正确答法(背熟)
「代码主要是 AI 辅助写的,这是实话——2026 年写 1.8 万行 Go,不用 AI 是不现实的,我也不会假装是自己一行行敲的。
但我想区分两件事:AI 能帮你写代码,不能帮你决定架构。
这个项目里所有关键的设计决定都是我自己做的,而且每一个都能追溯到具体的理由。我举几个:
- Provider 接口为什么只有三个方法——因为抽象的成本在于每个实现方都要付出代价,接口应该贴着唯一调用方的真实需求长出来。我一开始想过加
CountTokens,后来砍了,因为 OpenAI 没有对应端点,加了就会有实现退化成「假装不支持」。- 为什么系统提示要拆成「可缓存」和「不可缓存」两段——因为 Anthropic 的 Prompt Cache 是按前缀命中的,把每轮都变的日期/git 状态混进去会让整段缓存失效。
- 为什么工具并发要「保序分批」而不是简单全并发——因为同一轮里的
write_file和go build有依赖,全并发会产生概率性的、最难查的 bug。- 为什么压缩要分两层,而且第一层不做 LLM 调用——因为第一层是零成本无损的,先用便宜的,不够再用贵的。
- 为什么权限引擎只做前四层、第五层交给 agent 编排——因为
permission应该是纯函数、无 IO、无 UI 依赖。这些决定 AI 不会主动帮你想。 你甚至要在 AI 给出一个「能跑」的版本之后,把它推倒重来——比如工具并发那个,AI 第一版写的就是简单的串行执行。
而且我也很清楚 AI 写的代码有什么风险——这个项目里我自己找出了好几个问题,比如: ① MCP 工具的权限通配规则实际上是失效的。根因是
extractTarget没有 MCP 工具的分支,导致匹配目标恒为空串,而 glob 匹配器对空串直接返回 false。这恰好是我 README 里宣传的用法。 ② 后台 Agent 的工具白名单里,Skill 工具名写的是小写load_skill,但实际注册的工具名是首字母大写的LoadSkill——大小写不一致导致这条白名单形同虚设。 ③ 我做了「会话持久化」和「上下文压缩」,但这两个包恰好没有单元测试——这是个错误的取舍,因为OffloadAndSnip和pickSelectedTail都是纯函数,非常好测。这些问题 AI 写的时候不会告诉你,得靠你真正理解每一层的数据流才能发现。」
备注
这段回答的三段结构是关键:
- 承认(不辩解)——建立信任
- 拿出「架构决策」的证据——5 个带理由的设计决定,证明「想」是你做的
- 拿出「review 能力」的证据——3 个只有读懂代码才能发现的 bug
第 2 点最重要:面试官问的核心其实是「你理解这个项目吗」。所以答案不是「我写了多少」,而是「我能讲清楚为什么」。这 5 个设计决定直接对应 02 文档里的「设计权衡」部分,请务必背熟。
第 3 点是最狠的一击:它把「AI 写的」变成了「我 review 过、我测试过、我找到了 AI 找不到的问题」。性质完全不同。
场景 2:「你简历上写 10 轮迭代上限,代码里是多少?」
⚠️ 这是硬伤。面试官如果打开你的 GitHub,一定会看到。
正确答法(背熟,主动纠正)
「代码里是 25 轮,简历上写的 10 轮是错的。
原因是我写简历时参照了 README 的描述——README 里写的是「最多 10 轮迭代」,那是早期版本的陈述,后来代码调到 25 但 README 没同步。这是我的疏漏,以代码为准:
agent.go里的常量maxIterations = 25,而且停止提示文案里也硬编码了 25。顺带说,这个不一致本身就是个应该修的问题:常量在
agent.go:150,而提示文案在agent.go:157里把 25 写死了——改上限要改两处,应该用fmt.Sprintf从常量生成文案。」
备注
为什么主动纠正比被抓住好 10 倍:
- 主动说 → 「这个人诚实、以代码为准、还发现了代码里的坏味道」
- 被抓 → 「这个人简历注水」
而且你顺手给出的「文案硬编码 25」这个细节,会让他觉得你是真的读过代码。
顺便检查:简历里其他数字都对吗?
| 简历表述 | 代码事实 | 结论 |
|---|---|---|
| 10 轮迭代上限 | 25(maxIterations) | ❌ 必须改口 |
| 连续幻觉检测 | 连续 3 轮整轮全未知才停(maxUnknownRun = 3) | 🟡 说准 |
命名空间 mcp<server><tool> | mcp__<server>__<tool>(双下划线) | 🟡 说准 |
| 30 天过期会话清理 | ✅ 30*24*time.Hour | ✅ |
| JSONL 追加写实时 fsync | ✅ file.Sync() 就是 fsync | ✅ |
| stdio + HTTP 双传输 | ✅ CommandTransport / StreamableClientTransport | ✅ |
| 五层权限防御 | ✅ 前四层在 Engine.Check,第五层在 agent | 🟡 讲清边界 |
| 从零手写 / 全 Go | 🟡 用了 4 类 SDK,「零」指不依赖 Agent 框架 | 🟡 必须准备 |
场景 3:「你说『从零手写』,可你这不是全是 SDK 吗?」
正确答法(背熟)
「『从零手写』这个说法我说清楚一下——指的是不依赖 Agent 框架,不是零第三方库。
我没用 LangChain、没用 Eino、没用任何 ReAct / Chain / Memory / Agent 框架。Agent 的运行时——循环、工具编排、权限、压缩、会话——100% 是自己写的。
我在三个地方用了库,而且是有意选择的: ① LLM 协议层用官方 SDK(
anthropic-sdk-go、openai-go)。流式 SSE 解析和tool_use增量拼接是纯苦力活,自己写只会引入 bug;而且这块正好是我要抽象掉的——SDK 藏在llm.Provider接口后面,agent包里 grep 不到任何 SDK 的 import。 ② MCP 用官方 go-sdk——协议细节(JSON-RPC、握手、传输 framing)不该重造。 ③ TUI 用 Bubbletea——Elm 架构,我只写 Model / Update / View。所以我的分层原则是:入口层(协议解析、传输、渲染)用成熟库,核心层(Agent 逻辑、权限、上下文)全手写。
如果面试官要验证,你可以直接 grep:
agent.go的包注释里就写了这条约束——『只依赖 llm、tool、conversation、permission,不 import SDK,保持协议无关』。」
备注
agent.go 的包注释:
// Package agent 承载 ReAct 循环编排:多轮调 LLM → 权限判定 → 执行工具 → 结果回灌,直到任务完成。
// 对外吐出一条 Event 流供 TUI 渲染。只依赖 llm、tool、conversation、permission,不 import SDK,保持协议无关。「不 import SDK」是一条可验证的设计约束——这比任何辩解都有力。
可能的追问:「那你觉得什么时候该用库、什么时候该手写?」
「我的判断标准是:这段代码是「我的产品差异点」还是「公共基础设施」。 协议解析、TUI 渲染、MCP 传输——这些是公共基础设施,做对了没有加分,做错了全是 bug,用库。 Agent 循环怎么收敛、工具并发怎么保序、权限怎么分层、上下文怎么压缩——这些是我的产品差异点,也是我真正要学的东西,手写。 还有一个现实标准:如果我写不过开源库,就别写。流式 JSON 解析我写不过官方 SDK;但 Agent 循环没有「标准答案」,那正是我要自己想的。」
场景 4:「这个项目有人用吗?有多少 star?」
⚠️ 这是典型的压力题。他在试探你是否会「吹」。
错误答法
❌ 「还在推广,马上就会有很多人用。」 → 吹牛。 ❌ 「没有,就是个玩具。」 → 自我否定。
正确答法
「这是个个人项目,没有真实用户——我不会编数据。
但我想说它在工程密度上不是玩具:
- 18 个包、101 个非测试文件、14,082 行 Go,加测试一共 17,984 行;
go test ./...全绿,11 个包有测试;- 有 CI(lint + test + 跨平台构建)、有 release 产物(darwin/linux/windows 三个平台);
- 每个能力都有对应的规格文档和验收清单——项目用 spec 驱动开发,13 个功能章节每个都有
spec.md/plan.md/task.md/checklist.md。而且它刻意覆盖了几个真实难点:协议差异的收敛、工具执行的并发与顺序语义、上下文的预算管理、权限的分层拦截、外部进程的生命周期管理。这些在任何一个要上线的 Agent 产品里都是核心问题,不是在简历项目里编出来的需求。
当然我也清楚它和生产的差距:没有多租户、没有可观测性(没有分布式 trace、没有指标上报)、沙箱是参数级而不是进程级、没有成本预算控制。这些我在项目文档里都写了。」
备注
这个回答的模板是:承认边界 + 搬出密度证据 + 主动列出生产级差距。
最后那段「主动列出差距」是反直觉但极其有效的——它把「被质疑」变成了「我在做自我评估」。面试官会觉得这个人有分寸感。
注意「有 CI / 有 release / 有 spec 文档」这些证据比「有多少 star」有力得多——因为它们说明你是按工程规范在做,不是写完就丢。
场景 5:「你这些代码里有没有你自己觉得写得不好的地方?」
⚠️ 这是「送分题变陷阱题」——如果你说「没有」,说明没有 review 能力;如果你说一堆不痛不痒的(「命名可以更好」),说明没深度。
正确答法
「有三个,按严重程度排:
第一,沙箱的层次错了。 我的沙箱是参数级的,只拦文件类工具的
path参数,对bash完全不生效——因为bash走的是CategoryExec,isFile是 false,沙箱那一段直接跳过。现在靠「bash 默认判 Ask、会弹窗」兜底,但一旦用户配了Bash全放行规则或者切 Bypass,就只剩黑名单那 10 条正则。 本质问题是我在用「解析工具的输入」来推断「进程会做什么」,这是不可能做对的事情——shell 的语义让它不可静态分析。正确做法是把沙箱下沉到进程级(Linux 用 bubblewrap/landlock,macOS 用 sandbox-exec)。第二,安全配置的失败方向反了。 权限配置文件 YAML 解析失败时,我降级成「空规则集」并忽略了 error。对安全配置来说这是 fail-open——用户写的
deny因为缩进错了解析失败,就静默消失了,用户还以为自己受保护。 应该 fail-closed:要么拒绝启动、要么至少给一个在 TUI 上可见的告警(现在只打到 stderr,在 TUI 里是被覆盖的)。第三,测试覆盖分布错了。
permission/agent/tool/mcp/hook这几个包有测试,但compact/session/llm这三个没有——恰恰是我简历上重点讲的「协议抽象」和「会话持久化+压缩」。而这三个模块里的核心逻辑大多是纯函数,非常好测,我当时判断「用 tmux 手工端到端验收性价比更高」是错的。」
备注
三个答案的共同特征:
- 不是「命名/格式」这种表层问题,而是「层次/方向/分布」这种结构性问题
- 每个都说清了「影响是什么」(不只是「有 bug」)
- 每个都给了具体的修法
面试官最想听到的是「你意识到自己当初的取舍错在哪」——比如第二条里的「fail-open vs fail-closed」,这直接展示了你理解安全设计的核心原则。
场景 6:「如果让你重做一遍,你会怎么做?」
这跟场景 5 有重叠。用这条收尾:
「我会改三件事,按重要性排:
第一,把沙箱从参数层下沉到进程层。 这是最重要的。 第二,把安全配置的失败方向改成 fail-closed。第三,把权限判定从「一次性静态检查」变成「可审计的事件流」——现在每次判定只产生一个
Decision和reason,用户看不到「经过了哪几层、每层的结论是什么、为什么最终是这个结果」。对安全系统来说,可解释性和审计能力跟拦截能力一样重要——出了事要能回答「为什么这个操作被放行了」。这三件事的共同点是:它们都不是「加功能」,而是「改变层次」。 我觉得这是我从这个项目里学到的最重要的东西——Agent 工程里最难的不是写功能,而是在一堆互相冲突的约束里找平衡点,并且知道自己的边界在哪。」
第二部分:⚠️ 项目里「定义了但没接线」的功能(高级答法)
这是你的秘密武器,也是最大的雷。
我逐个核过代码,发现项目里有 6 处「代码写了、但实际不可达」的功能。这些面试官几乎不可能自己发现(要读代码 + 推理调用链),但如果你主动说出来,效果是核弹级的:
它证明你真的理解自己的代码怎么跑起来的,而不只是「知道有哪些文件」。
发现 1:记忆自动更新功能实际不可达 🔴
事实
简历没写记忆功能,但 README 和项目文档里宣传了「自动笔记」——「Agent 每 5 轮或检测到『记住』关键词时,自动提取值得记住的信息」。
代码里触发逻辑是完整的(agent.go:350-357):
// 记忆更新触发(每 5 轮或显式请求)
if a.memMgr != nil && a.runtime != nil {
turnCount := a.runtime.IncTurn()
recentMsgs := extractRecentTurn(conv)
if turnCount%5 == 0 || hasMemorySignal(recentMsgs) {
a.memMgr.UpdateAsync(ctx, recentMsgs)
}
}但 UpdateAsync 永远会早退(memory/manager.go:71-78):
func (m *Manager) UpdateAsync(ctx context.Context, recentMsgs []llm.Message) {
go func() {
m.mu.Lock()
defer m.mu.Unlock()
if m.provider == nil {
return // ← 永远从这里返回
}
...因为 provider 恒为 nil——main.go 里构造时传的是 nil:
memMgr := memory.NewManager(projectMemDir, userMemDir, nil, "") // provider 待选定后设置而唯一的注入方法 SetProvider 在整个仓库里没有任何调用点:
// SetProvider 延迟注入 provider。
func (m *Manager) SetProvider(provider llm.Provider, model string) {
m.provider = provider
m.model = model
}grep -rn "SetProvider" 只命中定义处,没有任何地方调它。
结论:「记忆自动更新」这条链是死的——触发条件已实现,但执行不可达。memoryText 只反映既有的 MEMORY.md 内容,不会自动更新。
怎么用这个发现
如果面试官问「记忆功能怎么实现的」,不要吹,直接说:
「这里我要主动说一个我发现的实现缺口。
记忆的触发逻辑是完整的——主循环里每 5 轮或者检测到「记住/记忆/remember/memo」关键词就会调
memMgr.UpdateAsync。但这条链实际是断的:UpdateAsync的第一步是if m.provider == nil { return },而在main.go里构造 Manager 时传的 provider 就是nil——注释还写着「provider 待选定后设置」。问题是那个「设置」的调用点从来没有写过——
SetProvider方法定义了、有实现,但全仓库没有任何地方调它。为什么会这样? 因为启动时 provider 是在 TUI 里才确定的(用户可能要从多个配置里选一个),而
memMgr在 TUI 构造之前就建好了。这是个初始化顺序问题,我当时没解决就留了个 TODO,结果这个功能就一直是死的。修法有两种:① TUI 选定 provider 后调
memMgr.SetProvider(...);② 把NewManager改成延迟到首次UpdateAsync时再从某个地方取 provider(比如传一个func() llm.Provider而不是llm.Provider)。我倾向第二种,因为它不依赖调用顺序。这也说明一件事:「功能写了」和「功能能跑」是两回事。如果没有端到端验证这条链,光看代码是发现不了的。」
为什么这个回答是核弹级
- 它展示了你追调用链的能力——从
agent.go追到main.go再到memory/manager.go - 它给出了根因(初始化顺序问题),不是「不知道为什么」
- 它给出了两种修法并做了选择
- 最后那句「功能写了 ≠ 功能能跑」是一个工程洞察
发现 2:审批升级链的第 2 级没有调用点 🟡
事实
子 Agent 的权限审批有「三级设计」(agent.go:760-845):
case permission.Ask:
// 子 Agent dontAsk 模式:直接 Allow(spec F12.2)
if a.dontAsk { ... } // ← 第 1 级
// 子 Agent 升级到父 TUI 审批(spec F12.3)
if a.approvalUpgrader != nil { ... } // ← 第 2 级
outcome, ok2 := a.requestApproval(ctx, call, reason, ch) // ← 第 3 级ApprovalUpgrader 的定义和接口都在:
// ApprovalUpgrader 是子 Agent 把审批请求升级到父 TUI 的回调(spec F12)。
type ApprovalUpgrader func(ctx context.Context, req *ApprovalRequest) (permission.Outcome, bool)但 WithApprovalUpgrader 在全仓库(含测试)没有任何调用点——而 AgentTool 构造子 Agent 时也没传:
subAgent := New(t.parent.provider, t.parent.registry, t.parent.version, t.parent.eng,
WithRuntime(subRuntime),
WithAllowedTools(allowed),
WithSystemPrompt(def.SystemPrompt),
WithMaxTurns(def.MaxTurns),
WithPermissionMode(def.PermissionMode),
WithDontAsk(def.DontAsk),
WithHookEngine(t.parent.hookEngine),
// ← 没有 WithApprovalUpgrader
)结论:审批链实际是「第 1 级(仅 dontAsk 定义)→ 第 3 级(事件流)」,第 2 级形同预留。
怎么用
如果被问到子 Agent 的权限怎么处理:
「子 Agent 的审批我设计了三级的升级链,但我要诚实说:中间那一级实际没有接线。
三级设计是:①
dontAsk模式直接批准;② 通过ApprovalUpgrader回调把审批升级到父 TUI;③ 默认走自己的事件流。第 ② 级的接口定义了、实现路径也写了,但
WithApprovalUpgrader从来没有被调用过——AgentTool构造子 Agent 时没传这个选项。所以实际链路是 ① → ③。这个缺口的影响是什么? 子 Agent 的
Ask事件走自己内部的事件 channel——而那个 channel 的消费者(后台任务的aggregateEvent)只处理Tool和Usage事件,忽略Approval。所以后台子 Agent 一旦命中Ask,就会一直阻塞到 ctx 被取消为止。修法有两步:①
AgentTool里注入WithApprovalUpgrader,把请求转发到主 TUI;②task.Manager的事件聚合要处理Approval事件(但现在它跑在没有 TUI 的 goroutine 里,需要一条回主线程的通路)。这个也可能是刻意的取舍——因为子 Agent 默认跑在
ModeDefault下,而它大部分工具是只读的(Explore角色直接disallowedTools: [write_file, edit_file]),所以实际上很少命中Ask。但它是个定时炸弹。」
发现 3:后台子 Agent 会丢弃审批事件 🔴
(这是发现 2 的直接后果,可以合并讲)
事实
task/manager.go 的事件聚合:
func aggregateEvent(ev agent.Event, bt *BackgroundTask) {
if ev.Tool != nil && ev.Tool.Phase == agent.PhaseEnd {
bt.ToolCount++
bt.LastActivity = ev.Tool.Name
}
if ev.Usage != nil {
bt.Usage.Input += ev.Usage.Input
bt.Usage.Output += ev.Usage.Output
bt.Usage.CacheWrite += ev.Usage.CacheWrite
bt.Usage.CacheRead += ev.Usage.CacheRead
}
}只处理 Tool 和 Usage——Approval / Text / Err / Compact 全被忽略。
而 agent 侧遇到 Ask 会阻塞在 requestApproval 的 select 上等回复——而回复永远不会来(没有人处理那个事件)。只能等 ctx 被取消(TaskStop 或者父 ctx 取消)才能解锁。
怎么用
作为发现 2 的补充:「所以后台子 Agent 命中审批会挂死到超时。这是我核代码时发现的第三个 bug,和第二个是同一个根因链。」
发现 4:Fork 超时「切换后台」只是记账 🟡
事实
agent_tool.go 里前台子 Agent 有个 120 秒自动切后后台的机制:
const autoBackgroundDuration = 120 * time.Second // 前台路径
timeoutCtx, cancel := context.WithTimeout(ctx, autoBackgroundDuration)
events := make(chan Event, 32)
text, err := subAgent.RunToCompletion(timeoutCtx, subConv, prompt, events)
close(events)
if timeoutCtx.Err() != nil {
// 超时 → 交给后台任务管理器接管
id := t.taskMgr.AdoptRunning(ctx, subAgent, subConv, name, events, cancel)
return tool.Result{Content: fmt.Sprintf(`{"task_id":"%s","status":"timed_out_to_background"}`, id)}
}但 RunToCompletion 在超时后已经返回了(它检查 ctx.Err()),而且 close(events) 已经执行。所以 AdoptRunning 里那个 for ev := range ev 循环立刻退出,然后把状态标成 StatusCompleted。
结论:"timed_out_to_background" 这个状态语义上只是记账——子 Agent 实际上已经停止了,并没有真的「在后台继续跑」。
怎么用
如果被问到「子 Agent 跑了很久怎么办」:
「前台子 Agent 我设了 120 秒的自动切后台。但我要说清楚,这个机制的语义是有限的。
设计意图是「超过 120 秒就交给后台任务管理器接管,不阻塞主对话」。但实际实现上:
RunToCompletion本身会检查ctx.Err()并返回,而且返回前就close(events)了。所以AdoptRunning拿到的 channel 已经关闭,它的消费循环立刻退出,然后把任务标记为 completed。也就是说子 Agent 实际停止了,
timed_out_to_background只是一个记账状态,并没有真的在后台继续跑。要做对的话:需要把
RunToCompletion的 ctx 和「切换后台」解耦——不应该用一个「会 cancel 的 ctx」去跑,而应该用一个「独立的 ctx」,超时只是「通知 UI 不再等它」,不是「杀掉它」。这样才能真的后台继续。这个也是我核代码时发现的——光看「timed_out_to_background」这个状态名,会以为它真的在后台跑。」
发现 5:后台任务管理器没有清理机制 🔴(内存泄漏)
事实
task.Manager:
type Manager struct {
mu sync.Mutex
tasks map[string]*BackgroundTask // id → task
byName map[string]string // name → id
done chan string // 缓冲 32
counter int64
}全文件没有任何 delete(m.tasks, ...) 或 TTL 逻辑。
每个 BackgroundTask 持有一个 *agent.Agent、一个 *conversation.Conversation、Result 字符串——永不出栈。
怎么用
如果被问到「后台任务怎么管理生命周期」:
「任务有启动、取消、状态收敛、panic recover,但没有删除和 TTL。
tasks和byName这两个 map 里的条目永不出栈——一个长会话里跑几十个子 Agent,那些Agent、Conversation对象会一直常驻内存。Conversation尤其重,它持有完整的子 Agent 对话历史。这是个真实的内存泄漏。 修法很简单:加一个「完成 N 分钟(或保留最近 N 条)就删除」的清理,可以在
Launch的 defer 里做,或者加一个后台定时清理。要注意的是
byName的语义——它是「按名字找回任务」用的(SendMessage用它),所以删除的时候要考虑「用户还能不能给这个任务发消息」。我的设计是SendMessage只允许发给StatusCompleted的任务(相当于「继续对话」),所以删掉老任务会影响这个能力。得给一个明确的保留策略,比如「保留最近 20 个任务」。」
发现 6:Hook 的 SessionResume 事件从来没有被派发过 🟡
事实
Hook 系统定义了 11 个事件,其中一个是:
EventSessionResume Event = "SessionResume" // /resume 恢复完成、首条 user 之前但 grep 全仓库,EventSessionResume 只有枚举定义和 allEvents 列表里的引用——没有任何 Dispatch 调用点。
对照:EventSessionStart 有 2 个派发点(TUI Init 和 /clear 后),EventSessionEnd 有 2 个(/clear 前和进程退出兜底)。
怎么用
这个比较小,可以作为「我核代码时发现的一系列不一致」的补充:
「还有几个小的接线缺口。比如 Hook 系统定义了 11 个事件,但
SessionResume只有枚举定义、没有任何派发点——/resume恢复完成后不会触发它。而SessionStart和SessionEnd都有 2 个派发点。这是实现遗漏,不是设计取舍——恢复会话显然应该是这 11 个事件之一要覆盖的场景。另外
Notification事件注释里写「权限 Ask 弹出时 / Stream 返回 Err 时」,但代码里只有 stream error 一处派发点——权限 Ask 时没有派发。注释和实现不一致。」
第三部分:其他可能被挖出的缺陷清单
这些是「如果你被挖到,要能立刻承认并给修法」的清单。按危险程度排序。
| # | 缺陷 | 代码位置 | 危险度 | 一句话答复 |
|---|---|---|---|---|
| 1 | bash 能绕过文件沙箱 | permission/engine.go:Check | 🔴 高 | 沙箱是参数级不是进程级,靠「bash 默认 Ask」兜底(见 04 Q4.6) |
| 2 | MCP 权限通配规则失效 | permission/settings.go:extractTarget | 🔴 高 | fail-safe,只影响可用性(见 04 Q4.16) |
| 3 | 权限配置解析失败降级成空规则(fail-open) | permission/settings.go:loadSettings | 🔴 高 | 安全配置应该 fail-closed |
| 4 | 并发执行工具时没有 recover | agent.go:executeBatched | 🔴 高 | 工具 panic 会挂掉整个进程 |
| 5 | 记忆更新链不可达 | memory/manager.go + main.go | 🟡 中 | 初始化顺序问题(发现 1) |
| 6 | 后台子 Agent 丢弃 Approval 事件 | task/manager.go:aggregateEvent | 🟡 中 | 会阻塞到 ctx 取消(发现 3) |
| 7 | Fork 超时切后台是记账 | agent_tool.go | 🟡 中 | 语义名误导(发现 4) |
| 8 | task.Manager 无 TTL | task/manager.go | 🟡 中 | 内存泄漏(发现 5) |
| 9 | 「永久允许」的转义规则实际失效 | permission/persist.go:escapeGlob | 🟡 中 | matcherGlob 不认 \ 转义(见 04 Q4.14) |
| 10 | 自动/手动压缩后没重置锚点 | compact/compact.go | 🟡 中 | 只影响一轮的估算(见 05 Q5.25) |
| 11 | ContextWindow 写死 Providers[0] | cmd/mewcode/main.go | 🟡 中 | 多 provider 切换后会撞墙(见 03 Q3.9) |
| 12 | 子 Agent 的 model 参数被解析但没用 | agent/agent_tool.go | 🟡 中 | 声明了参数但没实现 |
| 13 | Registry.Execute 不校验白名单 | tool/registry.go | 🟡 中 | 模型幻觉调用白名单外工具仍能执行 |
| 14 | 两个包没有测试文件 | compact / session / llm | 🟡 中 | 见 00 软肋 1 |
| 15 | 没有重复工具调用检测 | agent.go | 🟡 中 | 死循环只靠 25 轮兜底 |
| 16 | TOCTOU 窗口 | permission/sandbox.go | 🟢 低 | 本地单用户场景威胁模型不成立 |
| 17 | resume.go 与 compact 的 token 系数不一致 | tui/resume.go vs compact/const.go | 🟢 低 | 3.5 vs 4.0,方向保守 |
| 18 | RegisterBuiltins 注释写 12 条,实际 13 条 | command/builtins.go | 🟢 低 | 注释与代码不一致 |
| 19 | MaxTokens 硬编码 4096 | llm/anthropic.go | 🟢 低 | 应该可配 |
| 20 | 没有命令行参数解析 / 信号处理 | cmd/mewcode/main.go | 🟢 低 | 没有 flag、没有 os/signal |
| 21 | async Hook 用 context.Background() | hook/engine.go | 🟢 低 | 不受调用方取消影响 |
| 22 | agent 侧 payload 的 session_id 恒为空 | agent.go:basePayload | 🟢 低 | 留了 TODO,只有 TUI 侧能拿到真值 |
| 23 | Hook 注入的 prompt 没有 <system-reminder> 标签 | agent.go:buildReminder | 🟢 低 | 只有 plan reminder 自己包了 |
| 24 | Plan 模式下 allowedTools 被忽略 | run_to_completion.go | 🟢 低 | 直接走 ReadOnlyDefinitions |
关于第 13 条(值得单独讲,因为它有安全含义)
事实
RunToCompletion 里按白名单收窄的是「给模型的工具清单」:
// run_to_completion.go
if mode == permission.ModePlan {
defs = registry.ReadOnlyDefinitions() // Plan 模式:allowedTools 被忽略
} else if len(a.allowedTools) > 0 {
defs = registry.DefinitionsFiltered(a.allowedTools)
} else {
defs = registry.Definitions()
}但 Registry.Execute 不做任何白名单校验:
func (r *Registry) Execute(ctx context.Context, name string, args json.RawMessage) Result {
t, ok := r.Get(name)
if !ok { return Result{Content: fmt.Sprintf("未知工具: %s", name), IsError: true} }
return t.Execute(ctx, args) // ← 只要注册过就执行,不看白名单
}后果:子 Agent 共享父的 *tool.Registry 指针(同一个实例),所以注册表里有全部工具。如果模型幻觉出一个不在白名单里的工具名,它仍然会被执行。
怎么用
「我的工具白名单是在「导出给模型的定义」这一层收窄的,不是在执行层。
具体说:
DefinitionsFiltered会把不在白名单里的工具从「发给模型的工具列表」里去掉,所以正常情况下模型看不到、也不会请求它们。但Registry.Execute本身不检查白名单——它只按名字查注册表,只要能找到就执行。这意味着:如果模型幻觉出一个白名单外的工具名(比如它在历史里见过),那个工具会被真的执行。
不过有个重要的前提:子 Agent 有独立的
Conversation(定义式是从空对话开始,Fork 式是克隆父消息),所以它看不到「父 Agent 用过哪些工具」——除非 Fork 式把父的工具调用历史也克隆过来了(这确实会发生)。正确的修法:把白名单校验下沉到执行层——在
Registry上加一个「当前允许的工具集」的概念,或者给子 Agent 用一个收窄的 Registry 视图。这是「在哪里做约束」的问题:我做了声明层(工具定义)的约束,没做执行层的。对安全来说,执行层才是必须守住的那一层。」
这段回答的价值:它又是一个「层次意识」的案例——约束应该做在执行层,而不是声明层。
第四部分:「我不会」的标准应对框架
面试官问一个你不会的东西时,他的目的不是看你知不知道,而是看你「面对不知道的东西怎么办」。
四种回答,从差到好: ❌ 编造 → 一旦被追问就露馅,定性为不诚实,直接出局 ⚠️ 「不会」 → 诚实但没信息量,浪费一次展示机会 ✅ 「不会,但我可以推测」 → 展示推理能力 ✅✅ 「不会,但我可以从我熟悉的东西推一下,并且说清我推测的依据和不确定的地方」 → 展示推理 + 边界意识
万能四步框架
① 明确承认不会(不绕圈子) ② 说出你知道的相关部分(展示知识面) ③ 给出你的推测和解法方向(展示推理) ④ 说明你的推测哪里可能错(展示边界意识)
示例 1:「你了解 LangGraph 吗?跟你的实现比怎么样?」
假设你真的不熟 LangGraph:
「LangGraph 我只知道它是 LangChain 生态里的多 Agent 编排框架,用图的方式定义状态机——但我没深入用过,所以具体的设计细节我不敢乱说。
我可以从我的项目出发推一下差异。 我猜核心区别在「谁控制流程」:
- 我的实现是代码驱动的——循环是 Go 的
for,停止条件是if,工具并发是WaitGroup。流程完全在我的代码里,模型只决定「调哪个工具」。- LangGraph 那种图编排应该是声明驱动的——节点和边用配置/装饰器定义,流程本身是数据。
如果这个推测成立,那权衡是:声明式的好处是可视化、可组合、容易做人在回路的中断恢复;坏处是抽象层会隐藏控制流的细节,遇到「我需要根据工具结果动态决定下一步走哪个节点」这种需求会很别扭。
我选择代码驱动的原因是:Agent 的控制流本身就很动态——一轮里有几个工具、哪些能并发、什么时候停,都取决于模型的输出和权限判定。把它压成静态图会损失表达力。
不过我的推测可能错在:LangGraph 可能已经考虑了动态性(比如条件边、动态路由)。如果你问我它在「检查点恢复」和「跨进程持久化」上怎么做,我确实不知道——而这两点恰好是声明式编排的优势,我的实现里只有 JSONL 存档,没有真正的 checkpoint 机制。」
为什么这段好:
- ① 明确说「没用过」(不装)
- ② 从自己熟悉的东西推理出对比维度(控制流的归属)
- ③ 给出权衡分析(不是「我的更好」)
- ④ 主动指出对方可能更强的地方(checkpoint 机制)
第 ④ 点是最关键的——它证明你不是在「护自己的项目」,而是在做技术判断。
示例 2:「你的 token 估算为什么不用 BPE 精确算?」
「精确的 BPE 分词我没实现。 我知道 Anthropic 有
count_tokens端点、OpenAI 有tiktoken,但两边都要额外依赖或者网络往返。我的判断是这个场景不需要精确。 因为我的估算方式是「锚点 + 增量」——锚点是 provider 返回的真实 usage,只有锚点之后新增的消息才靠字符数估算。所以每一轮都被真实值重新校准,误差不累积。而自动压缩的阈值留了 13000 token 的安全余量专门吃这个误差。
但这个判断有个前提:误差必须小于余量。如果误差超过 13000,就会出现「本以为没超但其实超了」的情况——那时候会靠紧急压缩兜底(收到
ErrPromptTooLong后压缩重发),用户会感觉到一次明显的卡顿。我不确定的地方:我没实测过这个误差在中文场景下的分布——我只知道中文按字节算会偏保守,但偏多少、极端情况会不会超 13000,我没有数据。如果有数据发现经常触发紧急压缩,那就说明余量不够,应该要么加大余量、要么上精确分词。」
第 ④ 点再次是关键:他承认「我没测过误差分布」。这是诚实,而且指出了一条明确的验证路径。
示例 3:「你这个权限系统,如果让你对抗一个专门的攻击者,能撑住吗?」
「撑不住,我不认为它能对抗有动机的攻击者。
因为这个系统的设计目标是「防止用户和模型一起犯不可挽回的错误」,不是「对抗恶意提权」。这两者的威胁模型完全不同:
- 前者的对手是「非对抗性的错误」——模型幻觉出一个
rm -rf /、用户手抖点了永久允许。对这种,参数级沙箱 + 黑名单 + 弹窗足够了。- 后者的对手是「有动机、会主动构造绕过的攻击者」。对这种,我的设计到处都是洞:沙箱对 bash 无效、路径检查有 TOCTOU 窗口、黑名单能被变量展开绕过、MCP 工具返回的内容可以做 prompt injection。
要做成能对抗攻击者的系统,需要三个层次的改变: ① 隔离层:进程级沙箱(namespace / seccomp / seatbelt),让子进程在操作系统层面就看不到项目外的路径; ② 能力层:不是「按工具名做黑白名单」,而是「能力令牌」——每次操作申请一个具体的能力(「读这个文件的这一段」),最小权限、用完即失效; ③ 审计层:所有操作留不可篡改的审计日志,出事能回溯。
这三条我一条都没做。 我的项目在这个维度上的价值是「把一个 Agent 该有的权限分层结构搭出来了」,而不是「解决了 Agent 安全问题」。这个区别我分得很清楚。」
这段回答的效果:它把一个「你的系统不安全」的质疑,变成了「我知道我的系统安全边界在哪」。 面试官要的就是这个。
第五部分:压力面模拟对话(含逐句台词)
下面是一段完整的压力面对话。请出声读一遍,感受节奏。
面试官:「你这个项目,我看代码量挺大的。都是你自己写的吗?」
你:「代码主要是 AI 辅助写的,这是实话——2026 年写 1.8 万行 Go,不用 AI 不现实,我也不会假装是自己一行行敲的。
但我想区分两件事:AI 能帮你写代码,不能帮你决定架构。这个项目里所有关键的设计决定都是我自己做的,而且每一个都能追溯到理由。比如 Provider 接口为什么只有三个方法、为什么系统提示要拆成可缓存和不可缓存两段、为什么工具并发要保序分批——AI 不会主动帮你想这些,你甚至要在它给出一个能跑的版本之后把它推倒重来。」
面试官:「那你说说,如果让你现在重新做一遍,你会在哪些地方做得不一样?」
你:「三个,按重要性排。第一是把沙箱从参数层下沉到进程层——我现在的沙箱只拦文件类工具的路径参数,对 bash 完全不生效……(展开 04 Q4.6)
第二是安全配置的失败方向反了——配置文件解析失败降级成空规则,这是 fail-open……(展开 06 场景 5)
第三是测试覆盖分布错了,compact 和 session 这两个包恰好没测试……」
面试官(打断):「等一下,你说 bash 能绕过沙箱?那这个权限系统不是形同虚设吗?」
你:「不是形同虚设,但确实缺了一块。 我说清楚它的实际防线在哪:
黑名单只对命令类生效,10 条正则拦的是不可逆灾难;沙箱拦文件类工具;而 bash 在 default、acceptEdits、plan 三种模式下都判 Ask——默认会弹窗让人确认。所以「默认配置下」用户是安全的。
风险窗口是:用户一旦配了 Bash 全放行规则、或者切到 Bypass 模式,就只剩黑名单那 10 条正则——而 cat /etc/passwd 是拦不住的。
所以我的结论是:这个系统的安全边界建立在「默认判 Ask」上,而不是建立在沙箱上。 沙箱只是一个补充。如果用户主动关掉了弹窗,我的防护就降级了——这是设计上的真实缺口。」
面试官:「那你为什么不做进程级沙箱?」
你:「因为跨平台成本和调试成本都很高,而我当时的判断是「参数级够用」。现在我认为这个判断是错的。
具体说,进程级沙箱要做:Linux 用 bubblewrap / landlock / seccomp,macOS 用 sandbox-exec(但这个已经被 Apple 标记为 deprecated),Windows 上更麻烦(Job Object + 受限令牌或者上 WSL)。三套实现、三套调试方式,而且隔离之后子进程的行为会变(比如找不到某些路径),排查问题会变难。
但我现在的看法是:这些成本是必须付的,因为「在应用层做路径校验」和「在内核层做隔离」是两个量级的事情——前者你永远在猜攻击者会怎么写命令,后者是操作系统帮你保证。」
(面试官停了一下)
面试官:「行。那你说说,你这个项目里最让你不满意的地方是什么?不是安全,别的也行。」
你:「最不满意的是「功能写了但没跑通」这件事。
我举个具体的:我实现了「自动记忆」——主循环里每 5 轮或者检测到「记住」关键词就会提取信息存成笔记。这条触发链我写得很完整,但整个功能实际是死的。
因为 UpdateAsync 的第一步是 if m.provider == nil { return },而我在 main.go 里构造这个 Manager 时传的 provider 就是 nil——注释还写着「provider 待选定后设置」。问题是那个「设置」的调用点我从来没写过——SetProvider 方法定义了、有实现,但全仓库没有任何地方调它。
根因是初始化顺序:provider 要到 TUI 里用户选完才确定,而 memMgr 在 TUI 构造之前就建好了。我当时留了个 TODO 就没管。
后来我核代码时才发现——这让我意识到一件事:「功能写了」和「功能能跑」是两回事。光看代码是发现不了的,必须端到端跑一遍。 这也是我现在更重视端到端验证的原因。」
面试官:「有点意思。那你怎么保证别的功能不是同样的情况?」
你:「我不能 100% 保证,但我可以给你我现在的做法和已知的清单。
做法上:我最近把整个项目核了一遍调用链,专门找「定义了但没接线」的东西。找到的除了刚才说的记忆更新,还有几个: ① 子 Agent 审批升级链的第 2 级——WithApprovalUpgrader 没有任何调用点,导致后台子 Agent 命中审批会阻塞到超时; ② Fork 的「超时自动切后台」实际只是记账,子 Agent 已经停了; ③ 后台任务管理器没有 TTL,任务对象永不出栈,是个内存泄漏; ④ Hook 的 SessionResume 事件只有枚举定义、没有派发点。
这四条的共同点是:单看代码都「像是实现了」,要追调用链+推理数据流才能发现。」
面试官:「你这些是自己发现的,还是别人告诉你的?」
你:「自己核的。这也是我为什么现在能跟你讲这些——如果你让我打开 GitHub 现场看某个文件,我可能不记得每个细节;但如果你问「这个功能怎么跑的」,我能从入口追到出口,因为我在准备面试的这段时间把主要链路都走了一遍。」
这段对话的关键点
- 第一问就承认 AI 辅助 —— 建立诚实基调
- 主动抛出「重新做会怎样」的三点 —— 掌握主动权
- 被打断质疑时不慌,先把「实际防线在哪」讲清楚 —— 不辩解,讲事实
- 被问「为什么不做」时承认当时的判断是错的 —— 不讲理由找借口
- 「最不满意」那问给了具体案例 + 根因 + 洞察 —— 满分答案
- 最后一句关于「自己核的」 —— 化解「你是不是背了稿」的怀疑
第六部分:面试前 10 分钟自检清单
数字类(务必准确)
- [ ] 代码规模:101 个非测试文件 / 14,082 行,加测试共 17,984 行
- [ ] 包数量:18 个 internal 包
- [ ] 迭代上限:25 轮(不是 10!)
- [ ] 幻觉熔断:连续 3 轮整轮全未知
- [ ] 权限:5 层 / 4 档模式 / 10 条黑名单正则 / 3 层规则配置
- [ ] MCP 命名空间:
mcp__<server>__<tool>(双下划线) - [ ] MCP 超时:连接 30s / 调用 30s
- [ ] 压缩阈值:窗口 − 20000 − 13000
- [ ] 第一层阈值:单条 50KB / 聚合 200KB
- [ ] 摘要熔断:连续 3 次
- [ ] 保留原文:≥10000 token 且 ≥5 条
- [ ] Token 系数:3.5 字符/token
- [ ] 会话清理:30 天
- [ ] 工具超时:30s(bash 自身默认 120s)
话术类(必须能脱口而出)
- [ ] 「从零手写」 的解释(不依赖 Agent 框架,入口层用库)
- [ ] 25 轮 vs 10 轮 的主动纠正
- [ ] 沙箱对 bash 无效 的完整论述(含威胁模型和修法)
- [ ] MCP 通配规则失效 的根因
- [ ] 7 个包没测试 的承认 + 补救方案
- [ ] 「如果重做会怎么改」 的三点
- [ ] 「记忆更新不可达」 这个发现(核弹级)
心态类
- [ ] 不会就说不不会,然后用四步框架(承认 → 相关知识 → 推测 → 边界)
- [ ] 被质疑时先讲事实,再讲判断,不要辩解
- [ ] 主动暴露缺陷 —— 它能证明你做过 review
- [ ] 不吹用户量和 star —— 讲工程密度
最后三句
- 「以代码为准」 —— 当简历/README 与代码不一致时,永远这么答
- 「我知道我的边界在哪」 —— 这句话的杀伤力比任何技术细节都大
- 「功能写了 ≠ 功能能跑」 —— 这是你从核代码里得到的真实洞察
➡️ 下一篇:07-白板手写题与反问收尾.md