逐日AI
第 3 周 · D21约 4 小时

打包与发布:全局命令、配置目录、版本与更新,二十一天总复盘

把它变成别人也能用的工具:配好可执行入口与打包产物,把散在环境变量里的配置收进配置目录并保留覆盖顺序,处理版本与更新提示,写好 README 与演示,最后回头看这二十一层是怎么长成一个工具的。

今日目标 0/3

登录后可以勾选并保存进度。

今日目标

  1. 能把一个 TS 项目打包成可全局安装的命令行工具,并验证安装后真的可用
  2. 能设计配置的分层与覆盖顺序,让环境变量、配置文件与命令行参数各就各位
  3. 能把项目包装成可展示的作品:README、演示、已知限制

最后一天不加任何新能力,只做一件事:让别人也能用上前二十天做出来的东西。做完之后回到页面顶部把三条目标勾掉。

小白版讲解

转正与交付工牌:别人也能把他请去用

第一天我们给这位新同事发了工位和内线电话,二十天里他学会了读代码、改文件、跑命令、停下来问、记住上次的事、接别的部门的工具、按老师傅的手艺卡干活、把活分给几个临时工去并行做、把车间规矩焊成他绕不过去的流程、看懂图纸、还能给自己算一笔工时与花销的账。今天是他转正的日子,而转正真正的标志不是能力清单又长了一条,是别的部门可以把他借走

借走这件事有它自己的门槛,而且和能力无关。他得有一张工牌,别人刷一下就知道他是谁、哪个版本。他得能在别人的车间里干活,而不是只认自己那间。别人的规矩和他的习惯冲突时,得有一套说法决定听谁的。第一次被借走那天,如果他开口第一句是一串谁也看不懂的编号,这次借调基本就到此为止了。

这些事都很小,小到容易被当成「收尾的杂活」。但它们决定的是同一件事:这东西到底是一个作品,还是只是你机器上的一堆代码。 二十天写下来的所有机制对一个装不上、跑不起、第一屏就报堆栈的工具来说,价值精确地等于零。

今天要做四件「不做就等于没做完」的事:可执行入口与打包产物、配置的分层与来源、版本与更新提示、第一次运行的引导。收尾是一条必须真跑的验证链。

第一道坎:TypeScript 怎么进包

这是所有 TS 命令行工具的第一个问题,而它的标准答案经常被绕过去:把 tsx 写进运行时依赖,入口直接跑 TS 源码。这确实能用,代价是每个装你工具的人都要跟着装一个几十兆的构建工具。零依赖纪律守了二十天,最后一天崩在这里很可惜。

所以本课的裁定是编译成 JS 再发出去:源码在 src,产物在 dist,包里只有产物。代价是多一个构建步骤,收益是用户装到的是能被 Node 直接执行的普通 JS。

但这一步有个坑:编译器只搬它会编译的文件。 src 下那些不是 TS 的东西——要被子进程起起来的脚本、模板、静态页、数据文件——它一个都不管。而本地开发跑的是源码,根本没用到产物,所以这类故障在本地永远复现不出来,只有装到别人机器上才报一句找不到模块。

修法是在构建脚本里自己搬一次,而且必须按目录遍历而不是手写清单。手写清单会在加第一个新文件的那天悄悄过期,没有任何东西会报错——本课的产物里正好有两个这样的文件,第十五天那个笔记库子进程和第十八天那个后台任务执行器,它们是在构建脚本写完之后才各自出现的,而没有人为它们改过那个脚本一个字。同一条道理决定了包清单怎么写:files目录,不列文件。

JSONJSON
"files": ["bin", "dist", "skills", "README.md", ".env.example"]

写成逐个文件枚举的那一刻,这个包就开始了它的过期倒计时。

入口:JS 交一个带 shebang 的文件,Python 交一个函数

全局命令要有一个入口。两种生态在这里的分工很不一样,对照着看一次,「入口」这个概念会清楚很多。

Node 这边,你在 bin 字段里指一个文件,安装器给它建一条符号链接。执行这条链接的是 shell 不是 node,所以第一行必须是 shebang;少了它报的是一句语法错误,完全指不到病根。Python 那边正相反:你在打包配置里声明一个可调用对象,启动器由安装器生成——你从来不写 shebang,因为那个文件不是你写的。

bin/mca.mjs
#!/usr/bin/env node
// 三件事:① 第一行的 shebang 不能少 ② 扩展名写 .mjs,不指望 package.json 的 type
// ③ 一行业务都不写——bin 路径是发布契约的一部分,公开之后就不好改了
import '../dist/index.js'

两边还有一条一样的纪律:版本号只能有一个真源,Node 是包描述文件、Python 是包元数据。代码里另写一个版本常量,发版时改一处忘一处,用户报的 issue 就会全部带着错的版本号——这种 bug 不会让任何测试变红。

配置分层:作用范围越窄的越大

前二十天 mca 的配置只有一层:环境变量。够用是因为只有一个用户、一个仓库、一个终端。装到别人机器上之后,同一个值会同时有四个人想说了算。

内置默认值什么都没配时的兜底 用户配置这台机器上我个人的默认 项目配置这个仓库的团队约定 进 git 环境变量这个终端会话 命令行参数就这一次 最终生效的值并且记得自己从哪一层来
Mermaid 源码
mermaidmermaid
flowchart TD
  A[内置默认值<br/>什么都没配时的兜底] --> B[用户配置<br/>这台机器上我个人的默认]
  B --> C[项目配置<br/>这个仓库的团队约定 进 git]
  C --> D[环境变量<br/>这个终端会话]
  D --> E[命令行参数<br/>就这一次]
  E --> F[最终生效的值<br/>并且记得自己从哪一层来]

优先级就是这个顺序,而它不是品味是推导:作用范围越窄的越大。 范围越窄,说明用户越明确地在说「就这一次」「就这里」。倒过来排的症状都一样——用户加了个参数却没生效,而且没有任何报错。

实现上只有一个要点:把五层排成一个有序数组依次覆盖,而不是写一串条件判断;加一层只要插一个元素。

比覆盖顺序更值得说的是第二件事:每个值都要记住自己从哪来。 配置分层最常见的支持问题不是「值错了」,是「我不知道它为什么是这样」。所以解析的产物不该只有值,而要每个键都带着来源,再配一条把它们全摊开的命令。这不是调试功能,是这套分层能不能被人用起来的前提。

src/ship/config-layers.ts
// 数组顺序就是覆盖顺序:后写的盖先写的,写的时候连来源一起写
for (const entry of stack) {
  for (const key of ['baseUrl', 'model']) {
    const value = entry.data[key]
    if (value === undefined) continue
    traced[key] = { value, layer: entry.layer, from: entry.from }
  }
}

顺带一个工程细节:前二十天所有读配置的地方读的都是环境变量,其中三个还是冻结文件。新加一层有两条路——把配置对象一路传下去(要改二十个文件,有的改不动),或者在入口处把最终值写回环境变量(改一处,下游一行不动)。本课选后者,前提是有且只有一个写入点且它在任何 provider 被造出来之前执行;没有这个前提,它就退化成「环境变量被谁改过谁也说不清」。

配置目录里放什么,以及绝不放什么

配置文件放两处:用户目录一份(我个人的默认,不进任何仓库),项目目录一份(团队约定,要进 git)。项目那份从当前目录往上找第一个带配置目录的祖先——人经常在子目录里敲命令,而项目级配置属于整个仓库;停在第一个命中的祖先不再往上翻,是为了让嵌套仓库有确定答案:离你最近的那份赢。

然后是今天最该记住的一条红线:密钥不进配置文件。

理由不是洁癖。项目配置要进 git,用户配置明文躺在家目录里,两者都会被备份工具、同步盘、截屏,以及一句「你把配置发我看看」顺走。而环境变量只活在这个进程里,.env 至少有一条人人都懂的规矩叫别提交它。

做法上有个容易错的分叉:读到密钥是忽略还是拒绝并说出来。必须是后者。静默忽略的结果是用户明明写了 key 却一直被告知没有 key,他会以为工具坏了。拼错的键名同理,也要说一句。另外打印配置时密钥必须打码:日志、截图、issue 里最常见的泄漏就是有人把配置输出整段贴了出来。

全局安装之后,三个根第一次分开

本地开发时,「代码在哪」「我在哪个仓库干活」「我的个人偏好在哪」是同一个目录,所以一个取当前目录的函数就够了。全局安装把这三件事彻底拆开:代码躺在安装前缀里,和用户八竿子打不着。

规矩只有一条:随包分发的东西一律从包的位置去找,用户的东西一律不从包里找。 找包的位置要从模块自身的地址向上找,而不是写死几层相对路径——源码和产物的层级今天恰好一样,换个构建配置就不一样了。这条一破,症状是「我自己跑没问题,别人装完找不到文件」,而且本地永远复现不出来,因为本地这三个根是同一个目录。

今天顺手用这条规矩把技能目录也扩成三个来源:随包分发的、用户自己的、当前仓库的,同名时离项目最近的赢——并且要说一句谁盖了谁。

两件不许添乱的小事:首次运行与更新提示

第一次敲一个刚装好的工具,是它唯一一次不需要争取就能得到的注意力,而这一刻能给的最差回应是一段堆栈。合格的人话要回答三件事:缺的到底是什么、敲哪条命令能补上(要能直接复制)、以及我不想补能不能先看看它长什么样——最后这条退路必须给,它是把「我再研究研究」变成「我先跑一下」的唯一办法。

更新检查的难点也不在怎么查,在怎么不添乱:做坏了能让启动多等两秒、离线时报红字、上游抽风时整个工具起不来,每一条都比「用户晚一周升级」严重。所以四条硬规矩:失败一律静默、必须带超时、一天只查一次、离线模式一次都不查。没有超时的网络请求,等于把别人的可用性接进了自己的启动路径。

src/ship/update.ts
export async function fetchLatestFromRegistry(name) {
  try {
    // 超时不是优化,是这个功能能不能存在的前提
    const response = await fetch(`${registry}/${name}/latest`, {
      signal: AbortSignal.timeout(1500),
    })
    if (!response.ok) return null
    const body = await response.json()
    return typeof body.version === 'string' ? body.version : null
  } catch {
    // 「这次没查到」和「查到了没有新版」在用户看来是同一件事:屏幕上什么都不多
    return null
  }
}

非交互模式:审批的默认答案必须是拒绝

装出去之后,这个工具立刻会被塞进人以外的地方:脚本、CI、git 钩子。而 REPL 是给人用的——无交互终端下标准输入立刻结束,程序打印一行提示就退出,退出码还是 0。这正是本课从第一天就在防的那个假绿,今天它从验收纪律变成了真实的产品需求:得有一个跑一次就退出的模式。

这个模式有条自己的纪律:审批的默认答案必须是拒绝。 交互模式里「没回答」是用户在犹豫,非交互模式里「没回答」是常态;当成同意等于给所有 CI 发了一张无限授权。要动东西必须显式加开关,而且被挡下时那句话要说给模型听——让它改走只读的路,而不是对着同一个调用原地重试。

最后一步:验证不是看它有没有报错,是装完再跑一遍

构建成功、打包成功、没有任何红字——然后包在别人机器上第一秒就挂了。原因永远是同一类:本地跑的是源码,发出去的是产物。

所以验证链必须真跑,四步都不能省:打成包、装到一个临时前缀、在一个新建的空目录里跑版本命令、再在同一个目录里跑一次真实任务。临时前缀是为了绝不碰用户的全局环境,空目录是因为当前目录和包毫无关系正是要验的那件事。跑完把临时目录和包文件一起删掉。

实验里这条链是五项全绿,包体积 192 KB(包内 134 个文件)——同一份代码上稳定可复现;安装耗时不可复现,取决于网络与缓存,所以那条脚本一个耗时数字都不打印。

源码导读

动手实验

🧪 D21 实验:把 mca 打成包、装到临时前缀、在一个陌生目录里跑通一次真实任务

代码位置:labs/my-coding-agent-21days/day-21-ship-it

五个练习点全是「本地全绿、装完就炸」的那一类:包清单逐个文件枚举、入口少了第一行、构建只编译不搬资产、分层顺序排反且丢了一层、密钥闸没装、第一屏给的是堆栈、更新检查能把启动带崩。起点代码原样跑是十四项过四项。

  1. 把包清单改成按目录枚举、给入口补上第一行,看第 2、3 项从红变绿。
  2. 在构建脚本里补上「按目录遍历搬非 TS 资产」那一段,看第 4 项打出来的缺件清单清空。
  3. 把配置分层的顺序排对、补上命令行参数那一层,看同一个值在四层里各赢一次,且每次都说得出来自哪一层。
  4. 装上密钥闸,看写进配置文件的密钥被拒绝并警告,而打印出来的配置里没有明文。
  5. 把第一屏换成人话三选一、把更新检查的异常就地吞掉,跑 MOCK=1 SELFTEST=1 pnpm start 看到十四项全过,再跑一次 pnpm verify:pack 看完整的打包验证链。

验收看五条勾:自检十四项全过;源码目录下每个文件在产物里都有对应物;包里有产物与入口、没有源码与 .env;不加授权开关时写操作被拒且文件一字未改;打包验证五项全绿且跑完临时目录都没了。

面试题

今天三道题,考的是分发与配置的工程判断:

  1. 把一个命令行工具发布成可全局安装的包,有哪些容易忽略的坑?
  2. 配置的分层与覆盖顺序你怎么定?密钥该放哪里?
  3. 把这个项目写进简历,你会怎么用三句话说清它的技术含量?

完整题干、分析过程与答题要点见本课面试题库的第二十一天。第一题最有区分度——多数人会答「记得写 bin 和 files」,能说出「本地跑源码、发出去的是产物,所以必须装完再跑一遍」并举出一类只在安装后暴露的故障的人很少。

检查清单与明日预告

  • 能说清 TS 项目为什么该编译成 JS 发出去,而不是把构建工具拖成运行时依赖
  • 能说出编译器不搬非 TS 资产这个坑,以及它为什么在本地永远复现不出来
  • 能解释包清单与搬资产为什么都必须按目录而不是按文件枚举
  • 能把配置五层的优先级推导出来,而不是背下来
  • 能说出为什么每个配置值都要记住来源,以及打印来源为什么不是调试功能
  • 能说清密钥为什么不进配置文件,以及为什么必须「拒绝并说出来」
  • 能说出全局安装之后哪三个根会分开,以及本地为什么复现不出这类 bug
  • 能背出更新检查的四条规矩,并说清没有超时的后果
  • 能解释非交互模式的审批默认答案为什么必须是拒绝

二十一天做出了什么

回到第一天:那时屏幕上只有一个提示符,敲一句话进去什么都不会发生。今天它是一条能装到任何机器上的命令,在别人的仓库里读代码、改文件、跑测试、停下来问你、记住上次说过的话、接别人的工具、按团队的手艺卡干活、把大活拆给子 Agent 并行跑、用钩子把「先读后改」这类规矩变成绕不过去的流程、把长命令扔到后台、看懂你贴给它的截图,还能拿一套基准集给自己打分并算清这一轮花了多少钱,并且随时可以回退。

中间那二十层每层只做一件小事。回头看,最难的一层不是流式解析也不是压缩,是审批——它第一次要求你回答「这个东西被允许做什么」,而那不是技术问题。最值钱的一层大概是事件日志:所有事都被追加记录下来之后,恢复、分叉、回退、评估、成本统计全都变成在同一份数据上做不同的读法。而最容易被跳过、跳过之后最伤的一层,就是今天这一层。

和真正产品级的 Coding Agent 差多远

诚实地说:差得不少,而且差的地方大多不在你以为的地方。

  • 编辑能力:我们只有精确替换。产品级要处理补丁格式、缩进漂移、同一处多次修改、大文件的局部改写。
  • 检索能力:我们的 grep 就是字符串匹配,真实仓库要的是语义检索与符号索引。
  • 并发与隔离:我们的子 Agent 是进程内的。产品级要处理真实的工作树隔离、冲突合并、跑到一半被杀掉之后的残留清理。
  • 稳定性:我们只覆盖了已知的那几类错误,而真实世界里网关会返回你从没见过的形状。
  • 安全:我们的审批门挡的是本机写操作,产品级还要考虑提示注入、越权读取、密钥外泄。

但这份清单本身就是收获:你现在能具体地说出差在哪里,而不是笼统地觉得「人家的更厉害」。

接下来往哪走

三个方向,按投入从小到大:

  1. 把它变成你真的每天用的工具。 先改今天 README 里点名的那条取舍(别往用户仓库里写沙盒),再按你的工作习惯加两条斜杠命令。日用一周比再读十篇文章有用。
  2. 补齐某一块的深度。 检索去看 RAG 课,上下文预算去看 上下文工程课,工具生态与经验复用去看 MCP 课Skills 课
  3. 把它讲成作品。 简历上写「实现了一个 Coding Agent」没有信息量,写「手写 SSE 解析与工具分片归并、用事件日志做会话恢复与分叉、用内容寻址快照做回退」才有。今天做的打包与 README 就是这件事的载体:一段能直接跑的演示,胜过一屏功能列表。

第一天那句话现在可以还给你了:模型没有记忆,也不会自己动手——是你写的那二十一层让它看起来像会

面试题库

  • 把一个命令行工具发布成可全局安装的包,有哪些容易忽略的坑?What is easy to overlook when shipping a command line tool as a globally installable package?
    国内高频海外高频进阶#packaging#cli#distribution

    分析过程 · 先想清楚再作答

    1. 这题在考「你有没有真的发过一次」。没发过的人会答「记得配入口字段和包清单」,发过的人第一句会是:本地跑的是源码、发出去的是产物,这两条路径不一样,所以必须装完再跑一遍。
    2. 怎么拆:按「打包时漏了什么」「运行时找错了地方」「验证方式不对」三类各说两条,最后给一条可执行的验证链。
    3. 第一类,打包时漏东西:包清单按文件枚举而不是按目录,加一个新文件的那天就悄悄过期;编译器只搬它会编译的文件,要被子进程起起来的脚本、模板、静态页一个都不会进产物;入口脚本少了 shebang,类 Unix 上执行它的是 shell 不是运行时,报的是一句指不到病根的语法错误。
    4. 第二类,运行时找错地方:全局安装之后「代码在哪」「用户在哪个仓库干活」「个人配置在哪」是三个目录,本地开发时它们恰好是同一个,所以这类 bug 本地一次都不会出现。规矩是随包分发的资产从模块自身地址向上找包根,用户的东西一律不从包里找。
    5. 第三类,验证方式不对:构建没报错不等于装得上。真正的验证是打包、装到一个临时前缀、在一个新建的空目录里跑一次版本命令和一次真实任务。用临时前缀是为了不碰使用者的全局环境,用空目录是因为「当前目录和包毫无关系」正是要验的那件事。
    6. 还有两条配套的:版本号只能有一个真源(包元数据),代码里另写一个常量会让所有 issue 都带着错的版本;发布前先想清楚运行时依赖,把构建工具拖进运行时依赖是最常见的一种膨胀。
    7. 可预期的追问:产物要不要进版本库;跨平台的换行与可执行位怎么处理;怎么让这条验证链在 CI 里跑。

    How to reason about it · think before answering

    1. This tests whether you have actually shipped one. People who have not answer "remember the bin and files fields"; people who have open with the real point - locally you run sources, what you ship is build output, and those two paths differ, so you must install it and run it again.
    2. How to break it down - two items each under "missing from the package", "looking in the wrong place at runtime", and "verifying the wrong way", then finish with a concrete verification chain.
    3. Missing from the package - a file list that enumerates files instead of directories goes stale the day someone adds a file; the compiler only moves files it compiles, so scripts spawned as subprocesses, templates and static pages never reach the output; and an entry script without a shebang fails because on Unix-like systems a shell, not the runtime, executes it, producing a syntax error that points nowhere near the cause.
    4. Wrong place at runtime - after a global install, "where the code lives", "which repository the user is working in" and "where personal config lives" are three directories, while during local development they happen to be one, so this class of bug never shows up locally. The rule is to locate bundled assets by walking up from the module's own location, and never to look for user data inside the package.
    5. Verifying the wrong way - a clean build is not proof it installs. Real verification is pack it, install it into a temporary prefix, then run a version command and one real task from a freshly created empty directory. The temporary prefix keeps the user's global environment untouched; the empty directory is the point, because cwd having nothing to do with the package is exactly what is under test.
    6. Two more - the version number must have a single source of truth in package metadata, since a duplicated constant makes every bug report carry the wrong version; and decide runtime dependencies deliberately, because dragging a build tool into them is the most common kind of bloat.
    7. Likely follow-ups - whether build output belongs in version control; line endings and executable bits across platforms; how to run this verification chain in CI.

    答题要点

    • 核心一句:本地跑源码、发出去是产物,构建通过不等于装得上,必须装完再跑一遍
    • 包清单与资产复制都要按目录而不是按文件枚举,否则加一个新文件就悄悄过期且不报错
    • 编译器不搬非源码资产;入口脚本第一行必须是 shebang
    • 全局安装后包根、项目根、用户目录三者分开,随包资产从模块自身位置上溯去找
    • 验证链:打包 → 临时前缀安装 → 空目录里跑版本命令与一次真实任务 → 清理,全程不碰全局环境
    • 版本号只有一个真源;别把构建工具拖成运行时依赖

    Key points

    • Core point - locally you run sources but you ship build output, so a clean build is not proof of a working install; install it and run it again
    • List directories, not files, in both the package manifest and the asset copy step, or it silently goes stale the day a file is added
    • The compiler does not move non-source assets, and the entry script's first line must be a shebang
    • After a global install the package root, project root and user directory are three different places; find bundled assets by walking up from the module's own location
    • Verification chain - pack, install into a temporary prefix, run the version command and one real task from an empty directory, then clean up without touching the global environment
    • One source of truth for the version; never drag a build tool into runtime dependencies
  • 配置的分层与覆盖顺序你怎么定?密钥该放哪里?How do you decide the layering and override order of configuration, and where should secrets live?
    国内高频海外高频进阶#configuration#secrets#cli

    分析过程 · 先想清楚再作答

    1. 这题在考你能不能把优先级推导出来而不是背下来,以及你对密钥的位置有没有明确立场。只答「命令行大于环境变量大于配置文件」是背的,加不上一句为什么。
    2. 怎么拆:先给排序的判据,再说实现上的两个要点,最后单独说密钥。
    3. 判据一句话:作用范围越窄的优先级越大。命令行参数只管这一次、环境变量只管这个终端、项目配置只管这个仓库、用户配置只管这台机器、内置默认值管所有情况——范围越窄说明用户越明确地在说「就这一次、就这里」。倒过来排的症状是「我加了参数但没生效」,而且没有任何报错。
    4. 实现要点一:把层排成一个有序数组依次覆盖,不要写成一串条件判断。加一层只要插一个元素,不用重排任何判断。
    5. 实现要点二:每个值都要记住自己来自哪一层,并提供一条把它们全摊开的命令。配置分层最常见的支持问题不是「值错了」而是「我不知道它为什么是这样」,所以打印来源不是调试功能,是这套分层可用的前提。
    6. 密钥的立场要明确:不进配置文件。项目配置要进版本库,用户配置明文躺在家目录里,两者都会被备份、同步盘、截屏和一句「把配置发我看看」顺走。密钥只从环境变量或一个明确不提交的本地文件来,再往上是系统钥匙串或云上的密钥管理。
    7. 还有一个容易做错的分叉:在配置文件里读到密钥应该拒绝并警告,不是静默忽略。静默忽略会让用户明明写了却一直被告知没有,他会以为工具坏了。同理打印配置时密钥必须打码。
    8. 可预期的追问:同一个键在两层里类型不同怎么办;配置文件坏了要不要让工具起不来;怎么让团队配置对新人零成本生效。

    How to reason about it · think before answering

    1. This tests whether you can derive the priority order rather than recite it, and whether you have a clear position on where secrets live. Answering only "flags beat env vars beat files" is recitation, with no reason attached.
    2. How to break it down - state the ordering principle, then two implementation points, then secrets on their own.
    3. The principle in one line - the narrower the scope, the higher the priority. A flag applies to this one run, an environment variable to this shell, project config to this repository, user config to this machine, and defaults to everything. A narrower scope means the user is being more specific about "just this time, just here". Reversing it produces the symptom "I passed the flag and nothing happened", with no error anywhere.
    4. Implementation point one - express the layers as an ordered array applied in sequence rather than a chain of conditionals, so adding a layer means inserting an element instead of rearranging logic.
    5. Implementation point two - every value must remember which layer it came from, and there must be a command that prints all of it. The most common support question about layered config is not "the value is wrong" but "I do not know why it is this value", so printing provenance is not a debug feature, it is what makes the layering usable.
    6. Be explicit about secrets - they do not go in config files. Project config is committed, user config sits in plaintext in the home directory, and both get carried off by backups, sync folders, screenshots and a casual "send me your config". Secrets come from environment variables or a local file that is explicitly never committed, and above that from a system keychain or a managed secret store.
    7. One more fork that is easy to get wrong - a secret found in a config file should be rejected with a warning, not silently ignored. Silent ignoring leaves a user who did write a key being told there is none, and they will conclude the tool is broken. Likewise, mask secrets whenever configuration is printed.
    8. Likely follow-ups - what to do when the same key has different types in two layers; whether a broken config file should stop the tool from starting; how to make team config work for a new hire with zero setup.

    答题要点

    • 排序判据是「作用范围越窄优先级越大」:命令行 大于 环境变量 大于 项目配置 大于 用户配置 大于 内置默认值
    • 实现成有序数组依次覆盖,而不是一串条件判断;加一层只插一个元素
    • 每个值都要带来源,并提供一条打印全部来源的命令——这是可用性前提不是调试功能
    • 密钥不进配置文件:项目配置要进版本库,用户配置明文在家目录,都会被顺走
    • 读到密钥要拒绝并警告,不能静默忽略;打印配置时必须打码

    Key points

    • Order by narrowness of scope - flags over environment variables over project config over user config over built-in defaults
    • Implement as an ordered array applied in sequence, not a chain of conditionals; adding a layer is inserting an element
    • Every value carries its source, and one command prints all sources - a usability prerequisite, not a debug feature
    • Secrets stay out of config files - project config is committed and user config sits in plaintext at home, and both leak
    • Reject and warn when a secret appears in a config file rather than ignoring it silently, and always mask secrets when printing config
  • 把这个自己实现的 Coding Agent 写进简历,你会怎么用三句话说清它的技术含量?If you put this hand-built coding agent on your resume, how would you convey its technical substance in three sentences?
    国内高频海外高频深入#portfolio#communication#agent-engineering

    分析过程 · 先想清楚再作答

    1. 这题表面考表达,实际考自我评估:你知不知道自己做的东西里哪部分难、哪部分只是体力活。答「实现了一个 Coding Agent」信息量为零,因为这句话调一个框架也能说。
    2. 怎么拆:一句讲边界与约束、一句讲最难的那两三个机制、一句讲验证与诚实的局限。顺序不能反——先说约束,后面的机制才有分量。
    3. 第一句给边界:零框架零 SDK,只依赖一个 OpenAI 兼容接口,流式解析、工具循环、审批、协议全部手写,运行时依赖只有一个读环境文件的库。约束本身就是信息,它排除了「调库拼起来」这种可能。
    4. 第二句挑最有区分度的机制,不要罗列功能清单。可选的有:手写增量解析与工具调用分片归并(分片索引的起点不可假设,所以归并必须按索引建字典);只追加的事件日志同时支撑会话恢复、分叉与回退;内容寻址快照做文件级回滚并能识别未记录的改动;渐进披露的技能加载把不相干轮次的开销降到只剩一行摘要。挑两三个,每个都带一句「为什么这么设计」。
    5. 第三句说验证与局限:怎么证明它真的能跑(离线剧本下的端到端自检、打包后装到临时前缀再跑一次真实任务),以及诚实地说清它和产品级的差距在哪几块。主动说局限比等人问出来强,而且能说清局限本身就是懂行的证据。
    6. 反面示范要能指出来:堆一串功能名词、引用参数量或跑分、宣称「接近某某产品」。这三种写法都会在追问第一层就塌。
    7. 可预期的追问:这些机制里哪一个你重写过一次、为什么;如果只能保留三个功能你留哪三个;线上用它时最先会坏在哪里。

    How to reason about it · think before answering

    1. On the surface this tests communication; underneath it tests self-assessment - do you know which parts of your own project were hard and which were just legwork. "Built a coding agent" carries zero information, because someone who wired up a framework can say the same sentence.
    2. How to break it down - one sentence on boundaries and constraints, one on the two or three hardest mechanisms, one on verification and honest limitations. The order matters, because the mechanisms only carry weight once the constraints are on the table.
    3. Sentence one states the boundary - no framework and no SDK, only an OpenAI-compatible endpoint, with streaming parsing, the tool loop, the approval gate and the protocol all written by hand, and exactly one runtime dependency. The constraint is itself information, because it rules out gluing libraries together.
    4. Sentence two picks the mechanisms with the most signal rather than listing features. Candidates - hand-written incremental parsing and merging of tool-call fragments, where the index cannot be assumed to start at zero so merging must key off a dictionary; an append-only event log that supports resume, fork and rewind at once; content-addressed snapshots giving file-level rollback plus detection of unrecorded edits; progressive skill loading that reduces the cost of unrelated turns to a single summary line. Pick two or three and attach one reason each.
    5. Sentence three covers verification and limits - how you proved it runs (an end-to-end self test on an offline script, plus packing it, installing into a temporary prefix and running a real task again) and an honest account of where it falls short of a production tool. Volunteering the limits beats being asked, and being able to name them precisely is itself evidence of depth.
    6. Be able to name the anti-patterns - a pile of feature nouns, quoting model sizes or benchmark scores, or claiming to be close to some shipped product. All three collapse at the first follow-up question.
    7. Likely follow-ups - which of those mechanisms did you rewrite once and why; if you could keep only three features which three; what would break first if you used it in anger.

    答题要点

    • 先说约束再说机制:零框架零 SDK、只认一个通用接口、运行时依赖只有一个——约束排除了「拼库」这种解释
    • 挑两三个有区分度的机制并各给一句设计理由,不要罗列功能名词
    • 可选机制:流式分片归并(索引起点不可假设)、只追加事件日志支撑恢复与分叉与回退、内容寻址快照、渐进披露的技能加载
    • 第三句讲验证:离线端到端自检 + 打包装到临时前缀后再跑一次真实任务
    • 主动说清与产品级的差距(编辑能力、语义检索、真实隔离、未知错误形状、安全边界),说得出局限才显得懂
    • 避开三种塌方写法:堆功能名词、引用跑分、宣称接近某个产品

    Key points

    • State constraints before mechanisms - no framework, no SDK, one generic endpoint, one runtime dependency - because the constraint rules out the "glued libraries" reading
    • Pick two or three high-signal mechanisms and give one design reason each instead of listing feature nouns
    • Candidate mechanisms - merging streamed tool-call fragments where the index start cannot be assumed, an append-only event log serving resume, fork and rewind, content-addressed snapshots, and progressive disclosure for skills
    • Third sentence is verification - an offline end-to-end self test plus packing, installing into a temporary prefix and running a real task again
    • Volunteer the gap to production tools - editing capability, semantic retrieval, real isolation, unknown error shapes, security boundaries - naming limits precisely is what reads as depth
    • Avoid three collapsing patterns - piling up feature nouns, quoting benchmark numbers, or claiming to be close to a shipped product

评论