M0 · 工程底座 —— 开发文档(面试向)#
这篇讲的是为什么这么做、做了哪些取舍,不讲具体代码。目标是读完能用大白话讲出来。
一句话概括#
M0 不做任何能演示的功能,只干一件事:把后面每个模块都要用到的”公共地基”先铺好——装环境、管配置、隔离用户、统一调模型、写好提示词、立好质量标准。地基不牢就上业务,后面每加一个功能都要重复操心这些事,而且出了 bug 会沉到最底层、最难查。
打个比方:AgentLoop(主角逻辑)是车,M0 做的是修路、加油站、交通规则。先修路,车才跑得稳。
1. 为什么第一步是”打地基”而不是先写 Agent#
做法:先做不起眼、不能演示的地基(M0),再做最小可跑的 Agent(M1)。
为什么:这个系统有一堆”每个功能都会碰到”的公共问题——比如同时来很多用户怎么不串号、调用大模型在哪统一管、配置密钥放哪。如果等业务写到一半才补这些,就得在每个功能里重复写一遍,或者大返工。不如一开始把这些公共问题集中到地基里解决一次,之后写业务就能又快又专注。
面试怎么讲:做系统先搭骨架;把”反复出现的公共问题”统一解决,别让它散落在每个功能里。
2. 环境:一次”被迫的版本降级”#
背景:拿到项目时,环境用的是 Python 3.14(很新)。但项目后面要用几个做向量检索和模型推理的库(faiss、torch 这类)。
做法:把 Python 降到 3.11,重建环境。
为什么:这些库通常要等一段时间才会支持最新的 Python。3.14 太新,很可能装不上——而这个坑要到几个月后做”向量召回”那步才会爆发,那时候再回头换版本,代价大得多。所以在地基阶段就提前把它解决掉。
取舍:3.10、3.11、3.14 之间选。挑 3.11——库支持最齐全,又比 3.10 新一点。
面试怎么讲:选技术栈不能只看”现在能跑”,还要看依赖库跟不跟得上;把高风险的坑提前排掉,别拖到后期。
3. 配置和密钥:什么放配置文件,什么不放#
做法:跟环境有关的东西(密钥、服务地址、可调的阈值)放进一个 .env 配置文件;真正的逻辑(提示词、评分规则)放代码里。.env 绝不提交到代码仓库,只提交一个不含真实值的”样例模板”。
为什么:
- 密钥提交到仓库是安全事故——一旦推上去,哪怕后面删了也可能已经被别人抓到。
- 配置和代码分开,换台机器、换环境时只改配置不改代码。
- 留一个”样例模板”,别人一看就知道要配哪些东西,又不会泄露你的真实密钥。
一个小插曲:一开始图省事,把整个数据目录都设成”不提交”,结果连那份讲清楚数据长什么样的说明文档也被一起排除了。后来改成:大的数据文件不提交(反正能用脚本重新生成),但描述数据的文档要留下。
面试怎么讲:配置和代码分离、密钥不进仓库、数据靠脚本复现——都是为了安全和”换环境能跑”。
4. 多用户隔离:为什么不能用”全局变量”记住当前是谁#
背景:这是个网络服务,同一时间会有很多用户的请求在跑;而且我们的主 Agent 还会”分身”出子 Agent 帮忙干活。系统得随时知道”现在处理的是哪个用户、他的文件该存到哪个文件夹”。
做法:用一种叫 ContextVar 的机制来记这些信息,而不是用一个普通的全局变量。
为什么:如果用全局变量记”当前用户”,会串号——
用户 A 的任务还没跑完,用户 B 的请求进来了,把全局变量改成了 B;结果 A 的进度推送给了 B,A 分身出的子 Agent 把文件写进了 B 的文件夹。
ContextVar 专门解决这种问题:每个任务都有自己独立的一份记录,互不干扰;而且当主 Agent 分身出子 Agent 时,这份记录会自动复制过去,子 Agent 天生就知道自己属于哪个用户、文件该往哪存。
配套:把”设置”和”用完还原”打包成一个自动化的小机制,避免手动设置后忘了还原导致下一个任务读到脏数据。子 Agent 分身时,会给它换一个独立身份(方便前端高亮”子 Agent 正在干活”),但让它和主 Agent 共用同一个输出文件夹(产物都归到一次会话里)。
面试怎么讲:很多用户同时在线时,不能用全局变量记”当前是谁”,会串号;要用”每个任务一份、互不干扰”的机制,分身出去还能自动继承。这个点很能体现对并发的理解。
5. “分身”机制的地基:主 Agent 和子 Agent 为什么要一模一样#
背景:这个项目的多 Agent 玩法是”分身”——子 Agent 是主 Agent 的完整复制,不是另外训练的专用小弟。
做法:调大模型的入口全项目只建一个、共用;主 Agent 和所有子 Agent 用同一个模型、同一套提示词。
为什么:既然是”分身”,那分身的脑子就得和本体一样。如果子 Agent 换个模型、换套提示词,它就成了”另一个人”,而不是本体的复制——那就退回到传统的”一堆专家各管一摊”的复杂编排,失去了”分身对主 Agent 完全透明、随用随分”的简洁。共用一个模型入口还顺便省掉了每次分身都重新建连接的开销。
一个例外:给 Agent 答案打分的”评委模型”是单独配的,用更强的模型、并把它调成”不发挥、只严格打分”。这不矛盾——评委不参与 Agent 干活,是离线打分用的。
面试怎么讲:“分身”和”一堆专家分工”是两条路。分身的好处是统一、透明、能层层嵌套,维护一套就行;代价是子 Agent 不能为某个子任务做专门优化。
6. 提示词怎么写:三个关键决定#
(1) 用标签把提示词分块
给大模型的指令,用 <角色>、<工作流程>、<工具规则>、<何时收尾>、<硬性约束> 这样的标签分成几块。因为大模型对这种带明确边界的标签更敏感,各块之间不容易”串味”,比纯用标题分段更稳。
(2) 把”必须收尾”当成头等大事 Agent 最容易、也最致命的毛病不是答错,而是不停手——反复调工具、绕圈子停不下来,既烧钱又慢。所以专门用一块反复强调:“信息够了就立刻给结论,别为了追求完美无限地查下去。”
(3) 硬性要求放最后,用户偏好运行时填进去 像预算、材质、黑名单这种硬要求放在指令的结尾——模型对结尾的指令印象更深、更会照做。用户的长期偏好(比如”不要塑料""喜欢小众”)则留一个空位,每次根据当前是哪个用户,临时把他的偏好填进去。这样一套模板对所有人通用,个性化靠”填空”实现。
面试怎么讲:写提示词是有结构的工程,不是堆字数。三个重点——分块更稳、把”防止停不下来”当一等大事、硬要求放结尾且个性化用填空。
7. 一个安全细节:防止用户”翻墙”读到不该读的文件#
做法:所有”在指定文件夹里按用户给的文件名取文件”的操作,都走一个统一的安全函数,确认最终路径还在允许的文件夹里,否则直接拒绝。
为什么:用户上传或读文件时,如果文件名填成一串”往上跳目录”的字符(俗称”路径穿越”),就可能越过限制读到系统里任意文件。这是网络服务的经典漏洞。在地基阶段就提供一个安全的取文件方法,业务层只要用它就默认安全,不用每个人自己当心。
面试怎么讲:用户能控制文件名的地方都要防”路径穿越”;而且把安全做进公共方法里,靠”默认就安全”,别指望每个调用的人都记得检查。
8. 质量标准:怎么算”这段代码写完了”#
做法:定了”三道关 + 一张完成清单”,并且代码风格交给工具自动管,不写一堆文字规定。
- 第一关·机器自动查:格式、风格、类型、单元测试,一键全过。
- 第二关·功能验收:这个里程碑该跑通的东西,真的跑通了。
- 第三关·人工过一遍:对着”铁律”和完成清单做代码审查。
为什么:风格规定靠人记一定会走样,交给工具自动检查最省心。类型检查一开始用宽松模式(不强求每个第三方库都标注清楚),核心模块再慢慢收紧——一上来就最严格会拖慢早期进度。
面试怎么讲:先定义清楚”什么叫做完”(完成清单),再把能自动化的检查都自动化、并尽量前置到提交前;要求一步到位会拖慢节奏,所以分阶段收紧。
9. 协作方式:分支怎么管,要不要用 worktree#
做法:每个里程碑开一个独立分支;不为整个项目长期开一个 git worktree(同一个仓库的多个并行工作副本)。
为什么:worktree 的好处是”同时开好几个工作副本并行干”。但这个项目是一个人、一步步来、里程碑层层依赖,长期开一个 worktree 只会变成”又一个开发目录”,白增负担。真正需要并行时再临时开——比如后期好几个互不相关的功能同时写、或者一边跑很慢的数据处理一边继续写代码。
面试怎么讲:工具要配工作方式。一个人串行开发,普通分支就够了;worktree 是为”真要并行”准备的,不为用而用。
M0 留下了哪些”以后都能复用”的东西#
| 留下的东西 | 解决的公共问题 | 谁会用到 |
|---|---|---|
| 多用户隔离机制 | 很多用户同时在线不串号,分身能继承 | 所有功能、主/子 Agent、进度推送 |
| 统一的调模型入口 | 主子一模一样、只建一次 | 主 Agent、子 Agent、打分评委 |
| 提示词配置 | 分块更稳、偏好用填空 | 主/子 Agent 组装 |
| 安全取文件方法 | 防路径穿越 | 涉及上传/读文件的功能 |
| 质量三关 + 完成清单 | 统一”算写完”的标准 | 每个里程碑 |
面试一句话总结:M0 体现的是”做系统先搭骨架”的判断力——把”很多用户同时在线怎么不串号、模型在哪统一调、配置密钥怎么管、安全怎么保证、什么叫写完”这些反复出现的公共问题,一次性收进可复用的地基,让后面写业务又快又稳。