dsh · 深度问答
学习指南 / 深度问答
Interview Mode · 覆盖整条学习路线

先自己作答,
再揭晓答案。

这里的 42 道题沿着学习路线的五个阶段展开,外加一组综合概念和一组常见坑。 用法和面试一样:读题,先在心里(或纸上)给出你的答案,再点击题目展开参考答案对照。 答不上来的题,就是该回到对应章节复读的信号。

00 · 综合概念 · 贯穿整条路线

综合概念

这 8 道题不依赖任何一步的具体操作,但每一步都在用它们。全对,说明心智模型已经建好。

Q1概念用一句话说明 dsh 的架构核心?点击揭晓

一切皆插件,跑在 上。没有特权核心:模型适配器、工具注册表、会话日志、agent loop 本身都是插件,从 cordis.yml 配置组合而成,每个部件都可替换。扩展 = 在插件树里挂一个新插件;注册 = 可逆的 effect,插件卸载时自动回收。

Q2概念turn 和 step 有什么区别?点击揭晓

一个 step = 一次模型请求 + 它调用的工具;一个 turn = 零到多个 step。turn 在认领第一个输入之前开启,在「不再欠任何东西」(没有待做的工具请求、没有新输入)时关闭。turn/*step/*user/messageassistant/*tool/* 都是落进日志的 durable session 事件。

Q3概念capability seam(能力缝隙)由哪三个角色构成?为什么一个角色不够?点击揭晓

三个角色:Service Definition(声明接口)、Service Provider(实现)、Consumer(使用者,通常是模型侧工具)。一个角色不构成 seam;新增一个能力意味着一次设计全部三个角色。shell / fs / llm / subagent 都是范例——换一个 provider,整个产品的能力随之切换:把 fs 和 subprocess 指向远程沙箱,Bash、PTY、LSP 会一起搬走,provider 不需要任何 fork。

Q4概念「model-visible ⟺ logged」是什么意思?它推导出什么规矩?点击揭晓

凡是进入模型请求的内容,必须能从 session 事件日志重建。deriveMessages() 从日志推导模型历史,fork、resume、transcript、telemetry、持久化全部从这条流派生,还有运行时不变量断言这一关系。

推论:新增一种模型可见的输入,必须先新增一个 session 事件——扩展 SessionEventMap,然后从日志渲染,而不是在请求前临时塞数据。

Q5概念Cordis 的五种事件派发模式分别是什么?点击揭晓
  • emit — 不等待,监听器按注册顺序观察,无返回值
  • waterfall — 中间件语义,监听器收到 (...args, next),有返回值;不调 next() 即短路
  • parallel — 等待所有监听器并行观察,无返回值
  • serial — 等待,按注册顺序执行,有返回值
  • bail — 监听器按注册顺序观察,直到某个监听器返回 bail 值;harness 里主力是前四种

派发模式是事件公共契约的一部分,新事件要用 JSDoc @mode 标注,生成目录会拿声明对照派发点检查。

Q6概念waterfall 监听器为什么必须调用 next()?什么时候可以不调?点击揭晓

waterfall 是 around-middleware,值通过 next() 的返回值传播。只观察或注解共享请求对象的监听器必须调 next() 委托下去;直接 return 就短路了整条链,后面的策略和默认实现都收不到。

例外:当监听器拥有这个决定权时,短路就是设计本身——比如审批策略监听器拒绝一次工具调用。

Q7概念profile 和 bundle 分别是什么?叠加顺序如何?点击揭晓

profile 是 Harness home 里的具名组合:列出它叠加的 bundle、安装的树外插件,以及用户自己的 cordis.patch.ymlwebheadless 是自带模板。bundle 是「Cordis 配置行 + 配置所挂载代码」的分发格式,保证它插入的内容仍可被上层 patch。

叠加顺序(应用到空条目列表):profile 列出的各 bundle → profile 的 cordis.patch.yml → home 级 patch → 命令行 --patch overlay。patch 按 id 整行替换配置,或插入新行。

Q8方法架构文档里「Where new behavior goes」表解决什么问题?点击揭晓

它把「我想加 X」映射到「注册到哪个扩展点」:加模型 provider → ctx.llm;加工具 → ctx.tools;拦截请求/工具/turn → agent/*tools/* 事件;加持久会话状态 → 扩展 SessionEventMap……共 20 行映射。铁律:新行为挂在文档化的扩展点上;改 agent-loop 本身必须同步更新架构文档

Q9动态从 0.1.2-alpha.1 到 0.1.5-rc.1,有哪些值得知道的结构性变化?点击揭晓
  • Session 格式 v0 → v3(头号变化)SESSION_FORMAT_VERSION 现为 3 且已随产品发布;新增相邻迁移包族(session-format-v0-to-v1 / v1-to-v2 / v2-to-v3),旧日志打开时按这条链升级为当前代,历史 generation 文件落盘后不可变;「哪个版本已发布」以新权威文档 docs/session-format-status.md 为单一真源。注意 v2→v3 迁移会翻译 PTC 与 preset 命名——持久化词汇不再承诺原样保留。
  • packages/examples 整组删除:组合示例集中在 apps/cli/config/examples/
  • 新增桌面应用apps/desktopapps/desktop-host
  • 持久终端 seam ctx.terminalstool-bash-persistent / tool-pwsh-persistent 让 cwd、环境变量与后台任务跨调用保留。
  • Remote API:Host 业务方法经 ctx.remote.<namespace> 暴露给客户端(packages/api,见 docs/api-gateway.md)。
  • patchReload 热重载:profile patch 的生命周期可选 live(保存即重载)或 startup(仅启动时应用一次)。
  • Agent Teams 成为公开的 opt-in seamctx.agentTeams):仍在 packages/experimental,但已是正式发布、按需开启的能力。
  • 版本号 0.1.4 被跳过:发布序列从 0.1.3 直接进入 0.1.5。

核心心智模型全部保持不变:一切皆插件、seam 三角色、只增会话日志、工具执行管线。变的都是「挂上去的新插件」——这本身就是微内核架构最好的注脚。

01 · Step 0 · 跑起来

Step 0 · 跑起来

先当用户,再当读者。这组题检验你的环境和第一印象是否扎实。

Q1实操从源码跑起 dsh 的前置条件有哪些?点击揭晓

Node.js 22.19+ 或 24+(CI 覆盖 22.19、24、26);Corepack 启用的 pnpm(仓库在 package.json 锁定 pnpm@11.7.0,版本不对时跑 corepack enable);Git 2.26+。可选:DEEPSEEK_API_KEY,用于 Web / headless / ACP 演示和真实 API e2e 测试。

Q2实操pnpm install 除了安装依赖还做了什么?缺失时怎么补?点击揭晓

它还会通过 scripts/install-lefthook.mjs 配置 worktree 本地的 Lefthook git hooks,以及 dsh-translation-pairing Git merge driver(用于双语档自动配对合并)。如果依赖是从缓存恢复、postinstall 被跳过导致两者缺失,手动执行 node scripts/install-lefthook.mjs 补齐。

Q3实操如何验证环境已经就绪?点击揭晓

跑一次 pnpm run typecheck,成功退出即 setup 完成。这也是之后每次改动前最基础的自检。

Q4实操第 0 步最该跑的三条命令是什么?各自的看点?点击揭晓
  • pnpm dsh web — Web UI(http://127.0.0.1:3080),像普通用户一样完成一次对话
  • pnpm dsh --profile headless "任务" — 无头一次性运行,适合观察纯日志输出
  • dsh --profile web --dump-config学习价值最高:打印你机器实际启动的插件树,其中任何一行都能被你自己写的 patch 替换
Q5陷阱没有 DEEPSEEK_API_KEY 会发生什么?key 应该怎么放?点击揭晓

Web / headless / ACP 演示跑不起来;真实 API e2e 套件会自动跳过(CI 也一样,不会因此变红)。key 写在仓库根目录的 .env(gitignored):DEEPSEEK_API_KEY=sk-...,可选 DEEPSEEK_BASE_URL(默认公共 API)。永不提交凭据

Q6概念为什么「先当用户再当读者」必须是第 0 步,而不是可选?点击揭晓

没有上下文的通读效率最低。先亲手触发一次核心流程,之后读代码时每个概念都有亲身体验可以挂靠;而 --dump-config 让「一切皆插件」从一句口号变成你眼前一棵真实的树——这是后续所有理解的支点。

02 · Step 1 · 三篇入口文档

Step 1 · 入口文档

先建词汇表,再看代码。这组题检验你是否真的读透了那三篇文档。

Q1方法三篇入口文档各自的分工和第一遍的读法?点击揭晓
  • docs/development.md — 读前半:setup 教程 + 命令表;contributor reference(Host/Client 双 aggregate 布局)第一遍略读
  • docs/cordis-primer.md读透:45 行浓缩了全部核心概念;想动手就配 docs/cordis-tutorial/
  • docs/architecture.md — 当地图反复回查,重点四块:profile/bundle 组合、核心包表、三类事件、Turn flow
Q2概念背出 Cordis 的五个核心思想。点击揭晓
  • 插件 = 实现 Service 的对象(带 inject / apply(ctx) 的函数,或 Service 子类)
  • context = 服务仓库:服务占用稳定的 ctx.<key>,其他插件按 key 发现服务
  • inject 声明服务依赖,加载顺序由服务需求表达
  • Typed events + 四种派发模式(emit / waterfall / parallel / serial)
  • 注册是可逆的 effect:ctx.effect() / ctx.on() 安装,卸载即回收
Q3概念为什么服务之间用 ctx key 互相发现,而不是 import 具体实现?点击揭晓

解耦提供者与消费者:消费者只依赖接口 key,provider 可以在配置层整体替换——本地 shell 换成远程沙箱,消费方一行代码都不用改。这是 capability seam 可替换性的根基。

Q4概念inject 解决了什么问题?点击揭晓

插件用 inject 声明所需服务后,会等到这些服务存在才被加载。加载顺序因此由服务依赖关系表达,不需要手写启动编排——这在「一切皆可替换」的世界里是刚需,因为你永远不知道某个服务由哪个 bundle 提供。

Q5概念架构文档把事件分成哪三类?各自的使用时机?点击揭晓
  • Session 事件 — 落盘并广播的持久事实;当这个事实必须活过 reload 时用它
  • Agent 事件agent/*)— 携带活 Agent 的在途事件(inbox、step、status、request…);要观察或拦截进行中的工作时用它
  • Capability 事件fs/*tools/*telemetry/*)— 不 import loop 就能给缝隙挂策略和适配器

选对事件域是大多数改动的第一个决策。

Q6陷阱cordis.yml 里写 JS 表达式有什么规矩?点击揭晓

只允许 !!js不是 !js),且只出现在插件 config 和条目 disabled 字段下;其余元数据保持字面量。需要按环境选择插件时,用 overlay 而不是在配置里塞条件表达式。

Q7方法读完三篇文档的自检标准是什么?点击揭晓

不看文档能回答四个问题:turn 和 step 的区别是什么?waterfall 为什么必须调 next()?加一个新工具注册到哪?想拦截一次模型请求该监听哪个事件(agent/request)?四题全对就可以进 Step 2 了。

03 · Step 2 · 顺主线读核心包

Step 2 · 核心包

沿 turn flow 读五个核心包。这组题检验你是否真的把主线走通了。

Q1方法五个核心包的阅读顺序和理由?点击揭晓

沿 turn flow 的顺序:core/session(事件日志,一切的源头)→ core/agent + core/agent-loop(Agent 接口与默认驱动)→ core/tools(注册表与执行管线)→ core/system-prompt(prompt 组装)→ llm/llm(消息词汇表与模型缝隙)。每个包配 docs/subsystems/ 下的对应页一起读。

Q2概念为什么 session 日志是「一切的源头」?点击揭晓

因为模型上下文从它派生:deriveMessages() 把日志投影成模型历史;raw assistant/chunk 事件保住 replay 和 UI 保真度。fork、resume、transcript、telemetry、持久化全部从同一条事件流派生——改日志就是改产品的记忆。

Q3概念agent/pre-step 这个事件的权力有多大?点击揭晓

它决定模型看到什么:监听器可以改写认领到的消息,也可以 outright 拒绝。注意一个细节:被拒绝或为空的首批消息,会关闭一个没有花费任何 step 的 durable turn——日志仍然记录这次尝试,审计不丢。

Q4概念一次工具调用要经过哪些关卡?点击揭晓

tool/call*tools/pre-executetools/executetools/post-executetool/result*。三段 tools/* 都是瀑布事件:审批、执行策略、沙箱包装都挂在这些事件上拦截,不需要进工具实现本身。

Q5方法读代码时定位「谁生产/谁消费某事件」用什么工具?点击揭晓

两张生成的地图:docs/event-producer-consumer.md 列出每个事件的生产者与消费者;docs/module-graph.md 给包依赖图。另有 tool-catalog.mdconfig-catalog.mdcapability-seams.md 分别索引工具、配置字段、能力缝隙。先查地图再读码,效率差一个数量级。

Q6方法Step 2 的验收标准是什么?点击揭晓

能在代码里把「用户发消息 → 收到回复」的完整链路走一遍:指出每个事件在哪里 emit、谁监听;说清 system-prompt 的每个段落是谁注册的;解释一次工具调用从 tool/calltool/result 经过了哪些瀑布。能讲给别人听,才算走完。

04 · Step 3 · 动手改一点东西

Step 3 · 动手

理解要被代码验证。这组题检验你的第一次扩展是否走在正确的路上。

Q1概念为什么第一个练习是「加一个 tool」,而不是改 loop 或加 provider?点击揭晓

tool 是最小但完整的扩展练习:会碰到 ctx.tools 注册、JSON schema、三段瀑布执行管线、UI 渲染意图,但不需要碰 loop 本身——正好实践铁律「Plugins, not loop changes」。改 loop 门槛最高,加 provider 则需要先理解完整 seam。

Q2实操adding-a-tool.md 这篇 cookbook 覆盖了哪些要点?点击揭晓

最小形态 → execute() 契约规则 → 长任务如何处理 → 执行策略与观察 → PTC mode(原 Code Mode)如何免费用到你的工具 → UI 渲染(render intent:generic / terminal / diff 等,呈现方法是 args 的纯函数)→ 如何验证。照着做完,你就拥有了一个真实可用的扩展。

Q3概念「Plugins, not loop changes」的含义是什么?例外是什么?点击揭晓

新行为必须挂在文档化的扩展点上,而不是修改 agent-loop 的代码。例外:确实需要改 loop 时,必须同步更新 docs/architecture.md——那张地图是全仓库的公共契约,地图和代码不一致比 bug 更危险。

Q4实操想加模型 provider、shell 后端、人类命令,分别注册到哪?点击揭晓
  • 模型 provider → 适配器注册到 ctx.llm
  • shell 后端 → 注册 ctx.shell 后端;本地实现经 ctx.subprocess 派生进程
  • 人类命令 → 注册 ctx.commands,不经过模型 turn 直接分发

更多映射见架构文档的「Where new behavior goes」表。

Q5方法Step 3 的验收标准和进阶路径?点击揭晓

验收:你的工具出现在模型可用工具列表里并被成功调用,pnpm run typecheck 与相关测试通过。进阶:按 extension-cookbook.md 的索引挑战 adding-an-llm-adapter.md(provider 侧)、adding-a-package.md(完整包)、adding-a-settings-card.md(设置卡片);想碰平台层还有 adding-a-remote-api.md(用 ctx.remote 给远程客户端开 Remote API)与 adding-a-session-format-version.md(演进会话日志格式)。Web 端 Chat 节点主题已由 docs/subsystems/conversation.md 接管。

05 · Step 4 · 读「为什么」

Step 4 · 决策记录

代码是 what,决策记录是 why。这组题检验你是否会用这个仓库最独特的学习资源。

Q1方法Agent Notes 的正确读法是什么?点击揭晓

按需查阅,而非顺序通读:每当代码里遇到「奇怪但显然是故意」的设计,就去 .agents/notes/implemented/ 按分类(architecture / process / testing / bug-fix / feature / simplification)搜关键词。注意:已归档的笔记是冻结的历史,不能当作现行依据。

Q2方法推荐哪几篇 Agent Notes 作为入门?点击揭晓
  • event-sourced-sessions(architecture)— 为什么会话用事件溯源
  • capability-seams(architecture)— 为什么能力要拆成三角色
  • released-session-format-migrations(architecture)— 已发布的 Session 格式如何经相邻迁移升级旧日志,历史 generation 为何不可变
  • pnpm-over-yarn(process)— 包管理器选型
  • microkernel-event-taxonomy(architecture)— 微内核事件分类法

注意按分类到 .agents/notes/implemented/ 对应子目录里找;vendor-cordis-as-source 这类老笔记已移入 .agents/notes/archived/ 冻结存档,可以读,但不能当作现行依据。

Q3概念docs/postmortem/ 的价值是什么?点击揭晓

真实故障的事后分析(0001 起编号)。它展示团队怎么把一次事故提炼成一条工程规则——这是学「工程判断」而不是学「代码」的材料,也是判断一个项目工程文化成熟度的窗口。

Q4概念docs/glossary.md 收录了哪些核心词?什么时候查它?点击揭晓

capability-seam、agent-scope、goal、human command、loop hierarchy、Ralph 等。读代码或讨论卡壳时先查它——很多「看不懂」其实只是「这个词在仓库里有精确含义」。

Q5方法Step 4 的验收标准是什么?点击揭晓

在 GitHub Discussions 或 PR 里能跟上别人的论据——知道他们引用的「那条规则」出自哪篇 note;面对一个设计决定,能说出「为什么这样做」以及「为什么不那样做」。这一步没有终点,是伴随整个使用期的习惯。

06 · 常见坑 · 面试官最爱追问的

常见坑

这五道题是学习者最容易栽的跟头。能提前说清,说明你不只是读过,而是想透过。

Q1陷阱学这个仓库最常见的失败模式是什么?点击揭晓

跳过 Cordis 词汇直接读核心包代码——满屏 ctx.xxxinject、waterfall 全是天书。解法永远是回到 Step 1:先建词汇表,再读代码。

Q2陷阱想加功能时最容易走错的门是什么?点击揭晓

直接改 agent-loop。正确姿势几乎总是挂插件或监听事件;loop 变更不仅要同步架构文档,还牵动双 SDK 的投影和 snapshot 测试。先查「Where new behavior goes」表,九成需求都有现成扩展点。

Q3陷阱对测试门禁最常见的误解是什么?点击揭晓

以为 pnpm run test 就是 CI 门禁。CI 的覆盖率门禁是 pnpm run test:coveragepackages/*/*/src 逐文件 100%);而且模型或用户可见的行为变更,还必须在同一 PR 里补 keyless snapshot——包级测试和 mock fixture 都不能替代组装后应用的真实输出。

Q4陷阱对学习时间最常见的误判是什么?点击揭晓

两种极端:以为要通读 50 个包组、270 多个包才敢动手(不必,Step 0–3 集中 2–3 天就够);或以为 Step 4 可以「毕业」(它是伴随整个使用期的习惯)。路线设计本身就是为了避免这两种浪费。

Q5陷阱往仓库贡献代码时,「可配置性」上最容易犯的错是什么?点击揭晓

在插件里写死随部署变化的参数。仓库约定:这类选择必须是从 cordis.yml 可改的、带校验的 Config 字段——一个 DEFAULT_* 常量或测试钩子不算可配置性。只有协议常量、外部规范、安全不变量才允许固定。

Cordis 是什么?

一句话:Cordis 是一个「微内核」插件框架。dsh 没有一个包办一切的主程序——你看到的每一样能力,都是插在 Cordis 上的插件。

比喻一 · 配电箱与电器(微内核 + 插件)

Cordis 本体几乎不做任何具体的事,它只提供一排标准插座和一个总开关。模型适配器、工具注册表、会话日志、agent loop……全是插上去的电器:拔掉任何一个,其余照常运转;换一个同接口的,即插即用。

比喻二 · 公告栏与钥匙(ctx 与 inject)

插件之间不靠 import 互相认识,而是通过一块公告栏 ctx:提供服务的人把钥匙挂在写着名字的钉子上(ctx.llmctx.tools),要用的人按名字取钥匙,不关心是谁挂的。inject 则像入职申请上写明「我需要哪几把钥匙」——钥匙齐了才上岗,启动顺序由依赖自动排出,不用手写编排。

比喻三 · 可撕下的便利贴(effect)

插件的每一次注册都像贴一张便利贴,Cordis 负责记账;插件卸载时按账一张张撕掉,不留残胶。所以 dsh 连「在运行时改装自己」都是安全的。

cordis.yml 就是这个配电箱的布线图:哪件电器插在哪个位置,全写在图纸上——改图纸,就改了整台机器的构造。

延伸阅读 → docs/cordis-primer.md(45 行浓缩全部核心概念)