MewCode — 项目设计
源码:Bin-hy/EasyCoding(submodule:
projects/EasyCoding) 本文是技术设计文档:设计目标 → 需求分解 → 架构决策记录(ADR)→ 数据模型 → 接口契约 → 常量总表 → 测试策略 → 未完成项。阅读关系:
- 想看「怎么在面试里讲架构」→ 架构分层与依赖设计
- 想看「每个模块怎么实现的」→
03~11章- 想看「为什么这么设计、放弃了什么」→ 本文
一、设计目标与约束
1.1 目标
| # | 目标 | 衡量方式 |
|---|---|---|
| G1 | 从零实现一个可用的终端 Coding Agent | 能自主完成「读代码 → 分析 → 改多文件 → 跑测试 → 按报错修复」 |
| G2 | 不用 Agent 框架,把每个机制写到底 | 无 eino / langchain / 任何 Agent 框架依赖 |
| G3 | 协议无关 | 同一份上层代码同时跑通 Anthropic 与 OpenAI(含兼容端点) |
| G4 | 单二进制分发 | go build ./cmd/mewcode 产出无外部运行时依赖的二进制 |
| G5 | 安全可控 | 工具执行前有明确护栏;高危操作需人确认 |
| G6 | 长会话可用 | 连续工作数小时不因上下文超限中断 |
| G7 | 可扩展 | 工具/角色/钩子/技能四类扩展点都不需要改核心代码 |
| G8 | 可验证 | 每个能力有编号需求(F#)+ 验收标准(AC#)+ 测试 |
1.2 约束
| 约束 | 影响 |
|---|---|
| 单人开发、业余时间 | 优先做「难且能讲清楚」的机制,跳过工程化外围(CI 有了,但无压测/无 chaos) |
| 单机 CLI,非服务 | 不做分布式、不做多租户、状态放进程内存 + 本地文件 |
| 终端交互 | 需要 TUI(Bubble Tea),不能是纯管道式 CLI |
| 无 CGO | 排除 SQLite(除非用纯 Go 实现)、排除需要 cgo 的 tokenizer |
| 目标岗位是 Go | 刻意使用 channel / goroutine / interface / 泛型等 Go 特性作为练习 |
二、需求分解(13 章 → 6 大能力域)
| 能力域 | 章节 | 核心需求(摘) |
|---|---|---|
| Ⅰ 协议与交互 | ch02 / ch05 / ch10 | 双协议流式对话;系统提示模块化装配;Slash 命令体系 |
| Ⅱ 工具与执行 | ch03 / ch04 | Tool 抽象 + 注册中心 + 6 内置工具;ReAct 多轮循环 + 保序分批并发 |
| Ⅲ 安全护栏 | ch06 / ch12 | 五层权限防御;Hook 生命周期拦截 |
| Ⅳ 上下文与状态 | ch08 / ch09 | 两层上下文压缩;JSONL 会话存档;项目指令;自动笔记 |
| Ⅴ 扩展生态 | ch07 / ch11 / ch13 | MCP 客户端;Skill 系统;SubAgent + 后台任务 |
| Ⅵ 工程质量 | 贯穿 | Spec 驱动开发;go test -race;冒烟测试;CI 矩阵 |
三、架构决策记录(ADR)
格式:背景 → 决策 → 理由 → 后果(正面/负面)→ 替代方案
ADR-001 · 用 Go 而不是 Python
背景:Agent 生态以 Python 为主(LangChain / CrewAI / AutoGen)。 决策:用 Go 实现。 理由:① 单二进制分发,无运行时依赖;② channel/goroutine 天然表达事件流与并发执行;③ 静态类型能在编译期发现协议抽象的遗漏分支;④ 目标岗位(字节 Agent 开发)用 Go(Eino/CloudWeGo 生态)。 后果:
- ✅ 部署极简;并发原语贴合问题域;类型安全
- ❌ 生态少(要自己写 SSE 适配、自己写 frontmatter 解析);招人面窄 替代方案:Python + LangGraph —— 生态成熟但被框架绑架(框架帮你做了上下文管理与状态机,学不到底层),且分发麻烦。
ADR-002 · 不用 Agent 框架
背景:Eino / LangGraph / OpenAI Agents SDK 都能开箱提供 Agent Loop。 决策:全部手写。 理由:项目的第一目标是理解 Agent 的机制。框架把「循环怎么收敛、上下文何时爆、权限在哪一层拦」都封装成了黑盒,用完仍然不知道里面发生了什么。 后果:
- ✅ 每个机制都能讲到字节级;可以根据需求做框架不支持的取舍(比如「保序分批并发」很多框架没有)
- ❌ 开发量翻倍(1.7 万行);要自己踩坑(历史一致性、goroutine 泄漏) 替代方案:用 Eino 快速出 Demo —— 适合验证想法,不适合学习与面试深挖。
ADR-003 · Provider 接口的错误走 channel 而非返回值
背景:流式请求的错误可能发生在「连接建立后、事件流中途」。 决策:Stream(ctx, req) <-chan StreamEvent,错误放进 StreamEvent.Err,接口无 error 返回值。 理由:消费端只需要 for range + 一个 switch;不需要 select 两个 channel 并依赖「谁先关闭」的隐式约定。 后果:
- ✅ 消费端代码最简;取消语义统一(都靠 ctx)
- ❌ 「构造请求失败」这类同步错误也只能异步上报;错误类型被压平成
error替代方案:(<-chan StreamEvent, error)—— 只能表达建连前错误;或「事件流 + 独立 err channel」—— 约定更多。
ADR-004 · 事件流用单一结构体 + 非零字段分派
背景:Agent 需要向 UI 吐 9 类事件(文本增量、工具开始/结束、用量、迭代、提示、完成、错误、审批、压缩)。 决策:一个 Event 结构体,谁非零就渲染谁。 理由:避免 interface{} 多态事件导致的「每个消费者都要 type switch,新增事件类型要改所有实现」。 后果:
- ✅ TUI 侧一次
switch分派;新增字段向后兼容 - ❌ 结构体随功能增长(现 9 字段);「零值」与「未设置」不可区分 替代方案:
Event{Type EventType; Payload any}—— 类型安全但装箱分配 + 消费端断言。
ADR-005 · 工具执行按副作用「保序分批并发」
背景:模型一次可能请求 5 个工具。全串行延迟线性增长;全并发会让写操作顺序不确定。 决策:扫描调用序列,连续只读调用合并为并发批(sync.WaitGroup),有副作用调用单独串行,结果按原始下标回灌。 理由:模型给出的调用顺序含有语义(read A 在 write B 之前意味着「先看再改」),必须保持;同时只读工具之间没有依赖,可以安全并发。 后果:
- ✅
read×3的延迟 = 单个 read;结果顺序与模型请求顺序一致;并发只在只读集合内,无数据竞争 - ❌ 放弃了跨批重排的优化空间(
write B后面的read C本可与 B 并发,但不能) 替代方案:全并发 + 依赖分析(复杂且容易出错);全串行(慢)。
ADR-006 · 工具错误是「观察结果」而不是「异常」
背景:工具可能因为路径不存在、权限不足、命令失败而报错。 决策:所有错误包装成 ToolResult{IsError: true, Content: <结构化错误文本>} 回灌进对话历史,绝不中断循环。 理由:Agent 的本质是「根据反馈调整」。工具报错是有价值的观察——模型看到「文件不存在」会换个路径,看到「权限拒绝」会向用户求助。 后果:
- ✅ 循环鲁棒;模型能自我修正
- ❌ 错误可能被模型忽略(它可能不理会错误继续做别的);需要模型有足够的指令遵循能力 替代方案:错误直接
return err中断 —— 简单但 Agent 就退化成「一次成功或失败」的脚本。
ADR-007 · 权限做成五层流水线而非单一检查
背景:工具执行是完全裸奔的(能写任意路径、跑任意命令)。 决策:黑名单 → 路径沙箱 → 三层规则引擎 → 模式兜底 → 人在回路,命中即短路。 理由:每一层解决不同性质的威胁,且失败模式不同:黑名单防「已知恶意」,沙箱防「越界」,规则引擎表达「用户策略」,模式兜底给「默认安全姿态」,人在回路兜「未知风险」。 后果:
- ✅ 纵深防御;每层可独立测试与演进;Bypass 模式仍然保留前两层
- ❌ 复杂度高(1197 行);层间语义容易冲突(比如 allow 规则命中后就跳过了模式兜底) 替代方案:单一规则引擎 —— 简洁但无法表达「沙箱」这种非规则型约束。
ADR-008 · 上下文压缩做成两层而非一层
背景:工具结果占 token 的 80-90%,历史随轮次单调增长。 决策:L1 确定性落盘替换(零 LLM 成本)+ L2 LLM 摘要(花钱)。 理由:成本差异巨大。L1 是纯字符串替换,每轮都可以跑;L2 要发一次完整 LLM 请求。分离后可以先做免费的,只有免费手段不够时才花钱。 后果:
- ✅ 大幅降低成本与延迟;L1 失败不影响 L2
- ❌ 两套状态(
ContentReplacementState+AutoCompactTrackingState)增加复杂度 替代方案:只做摘要 —— 简单但每轮都花一次 LLM 调用;只做截断 —— 会丢用户需求。
ADR-009 · 替换决策冻结且保证逐字节一致
背景:同一个工具结果在每轮请求里都要被重新渲染成替换体。 决策:ContentReplacementState 记录每个 tool_use_id 的决策,已决策的永不翻转;替换体字符串存在账本里,之后每轮返回同一个 string。 理由:Prompt Cache 要求前缀逐字节一致。任何重新构造(map 迭代顺序、时间戳)都会让缓存全量失效,成本上一个量级。 后果:
- ✅ 缓存命中率稳定;决策可解释
- ❌ 落盘失败的项无法在本轮重试(要等下一轮);账本占内存且未持久化 替代方案:每轮重新决策 —— 实现简单但缓存全废。
ADR-010 · token 估算用「锚点 + 增量」而非 tokenizer
背景:需要在发请求前判断是否要压缩,但不想引入 tokenizer。 决策:EstimateTokens = anchor + ceil(新增字符数 / 3.5),锚点来自 provider 返回的真实 usage。 理由:① tokenizer 会带几十 MB 词表,破坏单二进制;② 每轮对全量历史分词有 CPU 开销;③ 锚点机制让误差只累积一轮(下一轮被真实 usage 校准回来)。 后果:
- ✅ 零依赖、零开销;误差可控(20-30%)
- ❌ 中文系统性低估(
len()是字节数);锚点必须包含CacheRead/CacheWrite,否则缓存命中时估算严重偏低 替代方案:调用 provider 的count_tokensAPI —— 精确但多一次网络往返。
ADR-011 · 系统提示模块化装配 + 缓存通道分离
背景:系统提示原本是一段揉在一起的常量,没有结构、无法按优先级排序、每轮全量重发。 决策:7 个固定模块(priority 10-70)+ 3 个可选槽(80/90/100),sort.SliceStable 按 priority 装配;System 拆成 Stable(可缓存)与 Environment(每轮变化)。 理由:① 模块化让「加一类指令」变成「加一个 Module」;② SliceStable 保证跨调用逐字节一致 → 缓存命中;③ 把「什么内容会变」变成类型级约束,而不是靠适配器猜。 后果:
- ✅ 可扩展、可缓存、可测试
- ❌
Module的 priority 是魔法数字,需要人工维护不冲突 替代方案:一整段 system + 适配器猜哪部分可变 —— 不可靠;或不做缓存 —— 长会话输入成本高一个量级。
ADR-012 · MCP 工具适配成内置 Tool 接口
背景:MCP 生态有海量社区工具,但它们的执行是远端/子进程。 决策:mcp/tool.go 把每个远端工具包装成 tool.Tool,命名 mcp__<server>__<tool>,走完全相同的权限链路、Agent 编排、UI 渲染。 理由:零特判 = 零漏洞面。如果为 MCP 工具开专用路径,权限、超时、截断、审计都要重做一遍,且很容易漏。 后果:
- ✅ 接入成本极低(改 YAML 即可);权限链路自动覆盖
- ❌ 实际存在一个漏洞:MCP 工具的
target提取不到 → 沙箱判定被跳过(因为isFile=false),规则Allow(mcp__srv__*)这类通配也匹配不上 替代方案:为 MCP 工具单独做权限 —— 更灵活但违背「统一链路」原则。
ADR-013 · SubAgent 用「独立运行时 + 共享基础设施」
背景:长任务后再做无关任务时,前序中间结果成为噪声;且无法并行。 决策:子 Agent 有独立的 SessionRuntime / Conversation / token 计数,但共享 Provider / Registry / PermissionEngine / HookEngine。 理由:隔离的是「状态」,共享的是「能力」。状态隔离防污染;能力共享避免重复初始化(连接池、规则加载)并保证权限策略一致。 后果:
- ✅ 上下文隔离 + 权限一致;Fork 模式借父对话命中 Prompt Cache
- ❌ 无并发上限(模型可一次启动无限子 Agent);子 Agent 的审批升级链未接线 替代方案:子 Agent 完全独立进程 —— 隔离更彻底但启动开销大、无法共享缓存。
ADR-014 · 会话持久化用 JSONL 追加写
背景:需要让对话历史在进程退出后可恢复。 决策:一行一条 JSON,O_APPEND 追加写,每条 fsync;压缩时写一条 compact 标记 + 全量快照,恢复时从最后一个标记之后加载。 理由:① 追加写崩溃最多丢最后一行;② 可观测(tail -f 就能看 Agent 在干什么);③ 零依赖(无 CGO/数据库驱动)。 后果:
- ✅ 简单、可靠、可调试
- ❌ 写放大(已知缺陷:
manageAuto每轮无条件ReplaceMessages→ 每轮写标记 + 全量重写,写放大 = 轮数 × 历史长度);无并发写保护;查询能力弱 替代方案:SQLite —— 查询强但引入依赖;或只在内存 —— 丢会话。
ADR-015 · Spec 驱动开发(每章五件套)
背景:单人项目容易「想到哪写到哪」,功能边界模糊、无法验收。 决策:每个功能章节先写 prompt.md → spec.md(编号需求 F# + 验收 AC#)→ plan.md → task.md,实现后写 checklist.md 逐项验收。 理由:① 把「想法」固化成可验证的需求;② 需求编号让「哪些做了、哪些没做」一目了然;③ checklist 是回归测试的素材。 后果:
- ✅ 13 章全部有据可查;范围蔓延被抑制(每章都有「不做的事」清单)
- ❌ 文档开销大(13 章 × 5 个文件);小改动也要走流程 替代方案:只写 README —— 快但边界模糊,容易做成「什么都沾一点」。
四、核心数据模型
// ─── 协议层(llm)────────────────────────────────
type Message struct {
Role string // user | assistant | tool
Content string
ToolCalls []ToolCall // 仅 assistant
ToolResults []ToolResult // 仅 tool
}
type ToolCall struct { ID, Name string; Input json.RawMessage }
type ToolResult struct { ToolCallID, Content string; IsError bool }
type Usage struct { InputTokens, OutputTokens, CacheWrite, CacheRead int64 }
// ─── 循环层(agent)──────────────────────────────
type Event struct {
Text string
Tool *ToolEvent
Usage *Usage
Iter int
Notice string
Done bool
Err error
Approval *ApprovalRequest
Compact *CompactEvent
}
type SessionRuntime struct { // 跨 Run 复用
Replacement *compact.ContentReplacementState
Recovery *compact.RecoveryState
AutoTracking *compact.AutoCompactTrackingState
Session *compact.SessionContext
ContextWindow int
UsageAnchor int64
AnchorMsgLen int
TurnCount int
ActiveSkills *skills.ActiveSkills
HookEngine *hook.Engine
PendingReminders []string
mu sync.Mutex
}
// ─── 执行层(tool / permission)───────────────────
type Result struct { Content string; IsError bool }
type Tool interface {
Name() string; Description() string
Parameters() map[string]any
ReadOnly() bool
Execute(ctx context.Context, args json.RawMessage) Result
}
type Engine struct { // permission
root string
blacklist []*regexp.Regexp
user, project, local RuleSet // 三层配置(local > project > user)
localPath string
startMode Mode
}
// ─── 状态层(compact / session)───────────────────
type SessionContext struct { SessionID, SessionDir, SpillDir string }
type ContentReplacementState struct {
mu sync.Mutex
seenIds map[string]struct{}
replacements map[string]string
}
type Entry struct { // JSONL 一行
Type, Role, Content string
ToolCalls []llm.ToolCall
ToolResults []llm.ToolResult
Timestamp int64
Model string // 仅首条
}五、接口契约与不变量
5.1 五个关键接口
| 接口 | 位置 | 方法数 | 抽象的是什么 |
|---|---|---|---|
llm.Provider | llm/provider.go:91 | 3 | 协议差异 |
tool.Tool | tool/tool.go | 5 | 工具能力(含 ReadOnly 副作用语义) |
agent.AgentCatalog | agent/agent_tool.go:22 | 3 | 子 Agent 角色目录(打破循环依赖) |
agent.TaskManager | agent/agent_tool.go:39 | 2 | 后台任务管理(打破循环依赖) |
agent.ApprovalUpgrader | agent/permission_upgrade.go:12 | 函数类型 | 子 Agent 审批升级 |
5.2 必须维护的六条不变量
| # | 不变量 | 违反后果 | 维护点 |
|---|---|---|---|
| I1 | 每个 tool_use 必须有配对的 tool_result | provider 返回 400 | executeBatched 取消分支补结果;Run 无条件 AddToolResults |
| I2 | 历史末尾不能是 tool 角色 | 模型不知道要干什么 | ensureAssistantTail |
| I3 | user / assistant 尽量交替 | Anthropic 严格校验 | joinAfterSummary 插 assistant 占位;appendReminderAnthropic 判断末条角色 |
| I4 | 替换决策冻结、替换体逐字节一致 | Prompt Cache 全量失效 | ContentReplacementState.DecideOnce |
| I5 | 恢复段的工具列表 == 本次请求的 tools 参数 | 模型以为自己有未提供的工具 | BuildRecoveryAttachment(snapshot, in.ToolDefs) 传同一份切片 |
| I6 | 锚点必须包含 CacheRead + CacheWrite | 缓存命中时估算严重偏低 → 压缩永不触发 | compact.UsageAnchor |
六、常量与阈值总表
面试时被问「这个数字怎么定的」,需要能说出依据。
6.1 循环与执行
| 常量 | 值 | 位置 | 依据 |
|---|---|---|---|
maxIterations | 25 | agent.go:150 | 复杂重构典型 8-15 轮,给 2 倍余量 |
maxUnknownRun | 3 | agent.go:151 | 给模型 1-2 次自我修正机会,3 次说明陷入固定错误模式 |
maxUnknownRunSub | 2 | run_to_completion.go:20 | 子 Agent 试错预算更小 |
planReminderInterval | 4 | agent.go:152 | 完整提醒 ~80 token,精简 ~25 token,按此频率平衡成本与遵循度 |
tool.DefaultTimeout | 30s | registry.go:13 | 单工具执行的兜底超时 |
autoBackgroundDuration | 120s | agent_tool.go:18 | 前台子 Agent 超时自动转后台 |
bash 内部 timeout | 120s | bash.go:76 | 实际被外层 30s 压制(已知缺陷) |
bash 输出截断 | 200 行 / 8000 字符 | bash.go:116 | 终端输出典型量级 |
read_file 截断 | 2000 行 / 256KB | read_file.go:78-80 | 单文件读取上限 |
glob / grep 结果上限 | 100 条 | glob.go:110 / grep.go:85 | 避免结果撑爆上下文 |
6.2 上下文管理
| 常量 | 值 | 位置 | 依据 |
|---|---|---|---|
singleResultLimit | 50000 字节 | const.go:7 | ≈14K token,超过就值得一次磁盘 IO |
messageAggregateLimit | 200000 字节 | const.go:9 | 单条 RoleTool 消息内聚合上限 |
SummaryReserve | 20000 | const.go:15 | 给摘要输出预留 |
AutoSafetyMargin | 13000 | const.go:17 | 吸收估算误差 + 单轮波动 |
ManualSafetyMargin | 3000 | const.go:19 | 手动/紧急路径只需保证摘要请求本身塞得下 |
recoveryFileLimit | 5 | const.go:24 | 文件快照数量上限 |
recoveryTokensPerFile | 5000 | const.go:25 | 单文件快照 token 上限 |
recentKeepTokens | 10000 | const.go:30 | 近期原文保留的 token 下界 |
recentKeepMessages | 5 | const.go:31 | 近期原文保留的条数下界(与上一个同时满足) |
maxConsecutiveAutoCompactFailures | 3 | const.go:36 | 熔断阈值 |
ptlRetryLimit | 3 | const.go:41 | 摘要自身撞墙时前 N 次每次丢 1 组 |
ptlDropPercentage | 0.2 | const.go:42 | 之后按比例丢 |
estimateCharsPerToken | 3.5 | const.go:47 | 中英混排经验值(偏保守) |
previewHeadBytes / previewHeadLines | 2048 / 20 | const.go:52-53 | 替换体头部预览 |
| 自动压缩触发线 | ContextWindow - 33000 | compact.go:109 | 200K 窗口 → 167K 触发 |
6.3 扩展与其它
| 常量 | 值 | 位置 |
|---|---|---|
connectTimeout / closeDeadline(MCP) | 30s / 5s | mcp/manager.go:20-21 |
| MCP 统一超时 | 30s | MCP 规范约定 |
| Hook 默认 timeout | 30s | hook/loader.go:174 |
maxIncludeDepth | 5 | instructions/loader.go:11 |
maxIndexSize(记忆索引) | 25KB | memory/manager.go:15 |
maxInstallFileSize / TotalSize / FileCount / Depth | 1MiB / 8MiB / 64 / 4 | skills/install.go:17-20 |
installHTTPTimeout | 60s | skills/install.go:21 |
环境采集 git status 超时 | 2s | prompt/environment.go:44 |
| 会话过期清理 | 30 天 | cmd/mewcode/main.go:119 |
| 恢复时会话暂停提醒阈值 | 6 小时 | tui/resume.go |
| 恢复时预压缩阈值 | cw - 8000 | tui/resume.go:57 |
| 记忆更新触发 | 每 5 轮 或 关键词 | agent.go:354 |
| 补全菜单最大行数 | 8 | tui/complete.go:13 |
| 默认上下文窗口 | Anthropic 200000 / OpenAI 128000 | config/protocol_defaults.go:12-14 |
七、测试与验收策略
7.1 四层验证
| 层 | 手段 | 现状 |
|---|---|---|
| 单元测试 | go test ./... | 15 个 _test.go,覆盖 agent/command/config/conversation/hook/mcp/permission/prompt/skills/tool。internal/tui 零测试(最大缺口) |
| 竞态检测 | go test -race ./internal/... ./cmd/... | CI 已跑(3 OS × 2 Go 版本矩阵);tui 包因无测试文件而未被实际覆盖 |
| 冒烟测试 | go run ./cmd/smoke | 非交互式验证完整 Agent Loop(需有效 API key) |
| 端到端 | tmux 中真实交互,对照 checklist.md 逐项验收 | 每章一个 checklist |
7.2 CI 流水线
# .github/workflows/ci.yml(触发:mewcode/** 变更 + push/PR 到 main)
lint: golangci-lint # working-directory: mewcode
test: go test -v -race -coverprofile=coverage.out ./internal/... ./cmd/...
matrix: [ubuntu, macos, windows] × [go 1.25, go 1.26]
build: 构建检查7.3 Spec 驱动的验收闭环
prompt.md → 需求意图(用户视角)
↓
spec.md → 编号功能需求 F1..Fn + 非功能需求 N1..Nn + 验收标准 AC1..ACn + 「不做的事」
↓
plan.md → 实现方案与模块划分
↓
task.md → 可执行的任务清单
↓
[写代码]
↓
checklist.md → 逐项对照 AC,标注通过/未通过为什么这套流程有效:spec.md 里的「不做的事」清单是关键——它显式地划定了范围,避免「顺便把 Y 也做了」导致的半成品堆积。例如 ch04 的「不做的事」里明确写了「权限系统 / 上下文压缩 / 沙箱」都留待后续章节。
八、未完成项与后续规划
8.1 路线图上的未实现功能
| 章节 | 功能 | 状态 |
|---|---|---|
| ch10 | LSP 集成(跳转定义、查找引用、诊断) | 📋 待排期 |
| ch11 | 插件系统(用户自定义工具加载器) | 📋 待排期 |
8.2 已知缺陷清单(按严重度)
| # | 缺陷 | 影响 | 严重度 |
|---|---|---|---|
| 1 | edit_file 参数名与权限层不匹配 | 工具用 file_path,权限层读 m["path"](permission/settings.go:122)→ ok=false → Engine.Check 返回 Deny「无法解析文件路径参数,安全拒绝」。核心编辑工具在主路径下一律失效 | 🔴 高 |
| 2 | 工具 panic 无 recover | 一个工具崩溃打穿整个进程 | 🔴 高 |
| 3 | MCP 工具绕过沙箱 | extractTarget 返回空 → 沙箱检查被跳过 | 🔴 高 |
| 4 | bash 不受沙箱约束 | 「最危险的通道防护最弱」 | 🔴 高 |
| 5 | ReplaceMessages 每轮触发全量重写 | 写放大 = 轮数 × 历史长度(二次增长),每条 fsync | 🟠 中高 |
| 6 | 子 Agent 审批升级链未接线 | WithApprovalUpgrader 定义但从未调用,子 Agent 遇 Ask 可能永久阻塞 | 🟠 中高 |
| 7 | llm 层零重试零超时 | 模型静默挂起 → 主循环无限等待 | 🟠 中高 |
| 8 | Agent 工具 120s 转后台阈值永不生效 | 外层 tool.DefaultTimeout=30s 抢先;且 Execute 返回即 cancel(),后台任务继承的 ctx 启动即失效 | 🟠 中高 |
| 9 | allowedTools 不是执行层硬约束 | 只裁剪发给模型的 schema,Registry.Execute 无白名单校验 → 可被提示注入绕过 | 🟠 中高 |
| 10 | MaxTokens 硬编码 4096 | 长输出被截断且 stop_reason 不被区分 | 🟠 中 |
| 11 | thinking 一旦用过工具就永久关闭 | 长会话拿不到扩展思考 | 🟠 中 |
| 12 | permission 前四层零单测 | 安全模块没有回归保护 | 🟠 中 |
| 13 | 规则匹配未归一化路径 | ./src/secret.go 可绕过 Deny(Write(src/secret.go)) | 🟡 低中 |
| 14 | escapeGlob 与 matchCommandPattern 语义不匹配 | 含 *?[] 的命令「永久允许」后规则永不命中 | 🟡 低中 |
| 15 | filter.go 工具名大小写不一致 | "load_skill" vs "LoadSkill" → 后台子 Agent 的 Skill 工具被静默剔除 | 🟡 低中 |
| 16 | 记忆索引启动读一次 | 新笔记要重启进程才进入系统提示 | 🟡 低中 |
| 17 | internal/tui 零测试 | 最重的并发消费者无测试覆盖 | 🟡 低中 |
| 18 | 子 Agent 无并发上限 | 模型可一次启动无限子 Agent | 🟡 低中 |
| 19 | git status 采集在 Run 起始一次 | 长任务中 git 状态不更新 | 🟢 低 |
面试建议:主动挑 3-4 条讲(推荐 1、2、5、6),每条说清「影响 + 修法」。缺陷 1 是最好的素材——它是「跨模块 key 不一致导致静默失效」的教科书案例,结论落在「分层必须有契约测试」,比单纯罗列功能有说服力得多。
8.3 如果要继续演进(下一步做什么)
| 优先级 | 事项 | 理由 |
|---|---|---|
| P0 | 修 edit_file 参数名不一致(核心编辑工具当前失效) | 功能底线 |
| P0 | 工具 panic 兜底 + llm 层超时/重试 | 稳定性底线 |
| P0 | 修 MCP/bash 的沙箱绕过;allowedTools 下沉到执行层 | 安全底线 |
| P1 | 抽公共 Agent Loop(消除 120 行重复) | 可维护性 |
| P1 | 修 ReplaceMessages 写放大 | 性能 |
| P1 | 补 permission + tui 单测(含跨模块契约测试) | 质量底线 |
| P1 | 修子 Agent 审批升级链接线 + 120s 转后台阈值失效 | 功能完整性 |
| P2 | token 预算控制替代单一轮数上限 | 成本可控 |
| P2 | 接入 OTel(trace + metrics) | 可观测,也是排障需要 |
| P2 | 评测集(任务级自动判据) | 质量闭环 |
| P3 | LSP 集成 | 代码智能 |
九、相关文档
| 文档 | 内容 |
|---|---|
| 项目首页 | 定位、能力地图、阅读顺序 |
| 01 · 项目全景与面试开场 | 三档自我介绍、简历写法、埋钩子 |
| 02 · 架构分层与依赖设计 | 六层架构、依赖图、接口契约 |
| 03 · Agent 内核与 ReAct 循环 | 循环、停止条件、保序分批并发 |
| 04 · 工具系统与执行编排 | Tool 接口、六工具、截断策略 |
| 05 · 权限与安全护栏 | 五层防御、规则引擎、已知缺陷 |
| 06 · 上下文工程与压缩 | 两层压缩、锚点估算、熔断 |
| 07 · 记忆、会话与项目指令 | 三层记忆、JSONL、@include |
| 08 · 扩展机制:MCP / Skill / Hook | 三种扩展点 |
| 09 · SubAgent 与后台任务 | 主从架构、Fork、任务管理 |
| 10 · 多协议抽象与提示工程 | 协议对照、缓存通道 |
| 11 · TUI 与 Go 并发模型 | Elm 架构、channel、锁 |
| 12 · 面试追问题库 | 120 问 + 手撕题 + 反问 |
| 13 · 企业级 Agent 平台方案 | 七个维度的生产化 |