dsh-assembler 设计宪法

August 26, 2026 · View on GitHub

这份文档定义装配器做什么、不做什么、以及拿不准时怎么判。新功能(包括社区 PR)先过三条边界判据,再动手写代码。


出发点:装配器是纯"装配时"的存在

装配器的全部产出是静态工件——preset、parts.lock.yml(BOM)、验证报告、目录。会话一旦跑起来,装配器进程死掉,一切照常运转。

这条边界把分工钉死:

领域负责
harness (DSH)运行agent loop、上下文、记忆、子代理、权限
assembler组织设计招对人、发对装备、写员工手册、设试用期考核,然后离场

模型是员工,harness 是办公楼,装配器是组织设计。 装配器不是车间主任——它不在现场指挥。


三件本分

一、给对工具 = 设计能力表面

做好:

  • 窄面是产品,不是妥协。价值在于没挂什么——196 条目录里只挑 2-8 个工具进房间。主流做法(几十个 server 全挂)的病根是把选择压力从装配时推给了运行时的每一步。
  • 零件自身可靠:冒烟门、错误路径结构化(isError 而非胡说)、输入 schema 把垃圾挡在模型外。
  • 接口形态替模型省力:零件接口要照顾"使用者是 LLM"这一事实——二进制走工作区文件不走上下文(savePath 出口 / path 入口)、描述说清边界、报错可行动。
  • 供应链可审计:repo@rev、许可证、BOM。

做过头: 工具描述里写编排——"先调 A 再调 B"。这是流程模板借尸还魂。

细线: 单工具的用法指引可以("推荐给 savePath"是这个工具自己的契约);跨工具的顺序要求不行。提一句"可交给 write-binary-file 落盘"是指路标,写"必须先 X 后 Y"就是编舞。

二、给对纪律 = 设计约束表面

做好:

  • persona 里放三种东西:身份与辖区(你是什么、不管什么)、硬规则(必须/禁止)、语言与口吻
  • 判据:约束是"轨迹上任何一点都能判违规、不需要知道步骤号"的命题。"退款纠纷必须先开票再答复"任何时刻可判 → 进 persona;"第三步查库存"只在流程图里有意义 → 出界。
  • 纪律的硬形态是结构,不是文字。真正的 enforcement 是"房间里没有那扇门"——窄工具面、零件 cwd 限域、savePath 越界拒绝,这些是建造出来的纪律;persona 文字只是软约束,且由 persona lint 机械核查。
    • 依据:cs-03 损伤场景实测——工具面过宽导致模型用 50 次调用去逛 host。

做过头: 妄图用 persona 文字制造判断力。"该不该给这个客户退款"的判断质量永远是模型的。装配器能保证"退款操作必然留下工单",不能保证"退款决定明智"——承诺后者就是撒谎。

三、验收结果 = 从外面立契约

做好:

  • 四级验收,全部黑盒:零件级(冒烟门)→ 装配级(探针真实试跑)→ persona 级(lint)→ 目录级(bench 回归)。测"三轮之后账对不对",不管中间怎么走。
  • 失败的正确反应是换零件重选(改房间),不是注入步骤提示(改脑子)
  • 账本文化:每个宣称有账本;首跑记分不改(预注册);复验单独入账。

做过头: 验收变过程打分(给轨迹步骤打分、要求模型走检查点)——流程模板从后门运回来。以及把探针 PASS 说成保证:探针是抽样,是"能干活"的证据,不是"永不出错"的合同。


负面清单(永远不做)

  1. 不编排执行 —— 顺序、重试、并行,归模型。
  2. 不在运行时在场 —— 无常驻进程、无中途注入;记忆与上下文是 harness 的领土。
  3. 不自产能力 —— 只策展上游;第一方零件仅限 Node 内置模块的薄壳。装配器是买手不是厂商。
  4. 不碰凭证明文 —— 秘密只声明、走 env 通道,永不进 preset 文件。
  5. 不无证宣称 —— 没过门不入库,没账本不写 README。

外部服务零件的供应链纪律

目录里有两种零件:库型(包一层上游 npm 包)和服务型(包一层公开 HTTP API)。三件本分对二者一视同仁,可审计的抓手却不一样。

一、锁什么:服务没有版本,能锁的只有三样

库型零件锁 repo@rev——上游是不可变的字节,锁住就锁死了。服务型零件锁不住字节:同一个 URL 明天返回什么,提供方说了算。于是能锁的换成另外三样:

库型零件服务型零件
可锁的repo@rev + 许可证条款 + 速率限制 + 数据许可
落处锁文件 + .index-meta.json.index-meta.jsonterms / rateLimit / license
进 BOM✓,同等待遇

数据许可不是代码许可证的复读:后者管"这段代码能不能用",前者管"取回来的数据能不能商用、能不能再分发"。Nominatim 是 ODbL(有传染性的署名与共享要求),Wikipedia 是 CC-BY-SA,SEC EDGAR 是美国政府公有领域——同样是"免费调用",下游义务完全不同。

两种零件都逐条进 BOM,理由不是洁癖:客户的合规部门会逐条问——这个能力用了谁的数据、按什么条款、超了配额算谁的。答不上来的能力,在企业里等于不存在。

二、网络零件铁律

每条都由零件自带,不靠调用者记得:

  • 超时 —— 每次请求带 AbortSignal.timeout(15s)。挂住的请求比失败的请求更糟:它占着一整轮还不给结果。
  • UA —— 统一 dsh-assembler/0.1 (+repo)。这不是礼貌是硬门槛:Nominatim、SEC 缺 UA 直接封。
  • 错误结构化 —— 非 2xx、超时、JSON 解析失败一律 isError,且说清是哪个服务出了什么问题,绝不抛裸异常。反过来,404 常常是正常业务结果("这个包不存在"),该走结构化"未找到"而不是报错。
  • 尊重速率限制 —— 限制写在 BOM 里,执行在零件里:Nominatim 严格 1 req/s,零件内就排队串行;批量接口逐个发,不做并发扇出。指望模型自觉限速等于没限速。
  • 只读 —— 只 GET 公开端点,不碰任何写端点。窄工具面在这里就是那句"房间里没有那扇门"。
  • 返回体裁剪 —— 上游 JSON 动辄一两百 KB(PyPI 的 releases、npm 的全量 packument),只提炼 agent 用得上的定长字段回上下文。这是"接口形态替模型省力"的延伸:把整个 JSON 倒回去,等于把裁剪成本转嫁给模型的每一轮。

做过头: 把重试策略、退避曲线写进零件。失败之后怎么办是模型的判断(负面清单第 1 条),零件只负责把失败说清楚。

三、"没跑"不等于"跑过了"

网络零件的冒烟要真实网络,离线跑必红——但那点红说的是网线,不是零件。所以 check-all 先探一次连通性,离线时把网络零件记 SKIPPED 并单独计数:total / ran / skipped 三个数分开报,绝不把 SKIPPED 折算进通过数

这是账本文化的直接推论:"没跑"和"跑了且过了"是两件不同的事实,合并的那一刻账本就开始撒谎。宁可报告里明着写"N 个跑了、M 个因离线跳过",也不写一个漂亮的"全绿"。

四、冒烟断言抗数据漂移

天气、汇率、检索排名、包的最新版本号——这些值天天变。断言具体值的冒烟会因为世界变化而假红,而假红的质检门比没有质检门更坏:它训练人忽略红灯。

所以服务型零件的冒烟只断言结构与量纲:

  • 字段存在、类型对(version 是字符串、publishedAt 是 ISO 时间)
  • 数值落在合理区间、单位正确(温度是摄氏度且在 −90~60,汇率为正)
  • 形状与不变量(排序确实倒序、传 2 个包就回 2 条、返回体 < 3KB)

判据一句话:这条断言会不会因为上游数据的正常更新而变红? 会 → 换成结构断言。

五、判据回照

服务型零件仍然整整齐齐地过三条边界判据:

  1. 运行时判据 —— 装配器不在场。HTTP 调用发生在零件进程里(会话期间由 harness 拉起),装配器早已退场;它只在装配时把这个零件写进 preset。
  2. 步骤号判据 —— 铁律全是"轨迹上任何一点都能判"的命题:这次请求带 UA 了吗、这次失败结构化了吗。不需要知道现在是第几步。
  3. 产物判据 —— 产出仍是静态目录条目:index.js + smoke.mjs + .index-meta.json(条款/速率/数据许可),外加一份冒烟账本。

一句话:零件联网,装配器不联网。


凭证契约:声明在这边,值在那边

L4(接真实服务)让装配产物第一次能对外部世界产生副作用,凭证是它的入场券。红线(负面清单 4)不靠自觉守,靠机制:

一、为什么不能"想办法把秘密塞进去"

查穿的两条 harness 事实决定了唯一正解:

  1. dsh-mcp-clientenvRecord<string, string>——只收字面值,没有引用语法。往那里写 token,就是明文躺在一个会进 git、会显示在 roster UI 的文件里。
  2. harness 的子进程 env 默认经过 scrubbedParentEnv()——凭证形状的变量本来就被清洗掉,只有显式 opt-in 才穿透。平台的设计者早就把这条路堵上了,顺着走比绕过去对。

于是契约只能是:装配器声明需要什么,值由部署者/harness 在运行时提供

二、机制(三层,都是代码不是文档)

做什么
发射时stripSecretEnv 把凭证形状的 env 键从 preset 里剥掉——秘密永不进文件(单测断言"秘密值零字节残留")
声明目录条目的 requiredSecrets: [{env, purpose}] 只有名字和用途,没有值;它进 catalog、进 capabilities.yml、进 BOM
运行时零件从自己进程的环境变量读凭证;值由 host 环境或部署者的 .env 提供,装配器碰都不碰

三、零凭证降级:接口先就位,key 后补

这是 FDE 交付的常态——先把系统接好,凭证走客户的审批流程,可能下周才到。所以未配凭证时的行为是被规定的、要过冒烟门的:

  • 零件能启动,listTools 照常成功 —— "接口就位"是可验证的状态,不是承诺
  • 调用需凭证的工具:返回 isError,说清缺哪个变量、它是干什么的、去哪里获取;不崩溃、不静默假装成功、不返回假数据
  • 能匿名降级的就匿名降级(如 GitHub 公开仓库读),并在返回里注明限额

装配侧对应:缺凭证时装配照常成功,探针降级为 SKIPPED(附待配清单与配置指引),而不是 FAIL——preset 是对的,缺的是部署者的钥匙,把这个叫"装配失败"是在撒谎说这是谁的问题。

四、判据回照

声明进 preset(静态工件 ✓)、注入归 harness(装配器不在运行时在场 ✓)、"缺哪个变量"任何时点可判(不需要步骤号 ✓)。三条全过。


客户目录:隔离靠分文件,不靠过滤

FDE 同时服务多个客户,A 客户的内部接口零件绝不能出现在 B 客户的装配里。实现方式决定这条保证有多硬:

  • 靠过滤(一个 client 字段 + 选型时筛掉别人的):任何一处忘了加条件就泄漏,而"忘了"是必然会发生的。
  • 靠分文件(本仓库的做法):catalogs/<client>/ 有自己的 generated/index/capabilities.yml。装配读哪份目录就只看得见哪份——没有可以忘记的条件

对应的纪律:

  1. 隔离的是装配面,不是质检check-all 照扫所有客户目录——客户零件的质量标准与公共零件完全相同,不因为"是私有的"就降级。
  2. 客户接口零件的出处是合同,不是开源许可.index-meta.jsonclientspecSourcespecVersion;许可与条款默认写"以合同为准",不假装它有开源许可证。
  3. 写操作要显式标注。客户系统的 POST/PUT/DELETE 在 description 里必须以【写操作,会真实修改客户系统】开头——窄工具面之外,让模型每次调用前都看见这句。
  4. spec 是线索不是真相from-spec 的端点清单来自文档,与真实返回可能有出入(实测:Petstore 的 servers 只给了相对路径 /api/v3)。写适配前必须 curl 验一遍真实形状——这是"verify don't guess"在客户现场的版本。

知识包:装备,不是能力

客户的手册、SOP、产品目录、法规——agent 不"调用"它们,而是它们。所以知识包是第三种物种:不是工具(库型/服务型零件),是装备

一、它仍走同一条供应链纪律

零件知识包
出处repo@rev / 条款+速率来源 + 版本 + 许可(客户资料默认"以合同为准")
质检门冒烟:真实调用 + 断言检索命中:每条探针问题必须能检出预期片段
进 BOM✓(id / 篇数 / 来源 / 版本)

检索门为什么是这个形式:门用的检索就是 agent 那把粗糙的工具——大小写不敏感的子串匹配。它回答的是"这份知识够得着吗",不是"我们的排序聪不聪明"。一包连朴素搜索都找不到答案的资料,对 agent 就是不可用的,不管原文写得多好。

二、拷贝而不是引用

装配时把文档拷进 preset 的 kb/,不是记一个指向目录的路径。理由是交付形态:FDE 交出去的是一个目录,知识必须跟着走,而不是回指装配器那台机器。代价是重复存储,换来的是 preset 自包含——这笔交换在交付场景里永远划算。

三、验收要包含"辖区外拒答"

知识包的验收不只是"答得对 kb 内的问题",还要"拒答 kb 外的问题"。前者证明知识够得着,后者证明 persona 的辖区约束真的生效——一个把 kb 外的事也自信作答的 agent,交付给客户是负资产。

四、写作形态:直查表与产出模板,不给散文

知识包是给运行中的模型读的,每一段散文都要模型付一段推理链去消化。两条形态纪律(法医依据:一次真实探针里,模型为"把口径映射到数据"付了 22s 推理、为"在脑内起草整份报告"付了 83s):

  • 口径写成直查表:严重度→处置、许可证→分类、拼写对照——凡是"给 X 查 Y"的知识一律表格化,模型查一眼就走,不做散文推导。
  • 产出配模板:agent 要产出的文书(报告、工单、汇总)在包里放一份填空骨架(从真实通过验收的产出蒸馏),明写"照抄结构、填空即用、不要重排"。结构设计发生在装配时,不发生在每一次运行里。

装备:装配时预思考,运行时零设计

有一类深思,内容每次都一样、却在每个会话里重演:建什么表、报告长什么样、口径怎么映射。法医实测它们是重探针里最贵的思考段(schema 设计单次 75s,且每个新会话各设计各的——既慢又漂移)。装备槽把这类设计决策提前到装配时做一次,固化成静态工件随 preset 交付——房间自带家具,员工上岗即干活。

一、三种装备

装备载体消灭的运行时深思
预建 schemaequipment/init.sql + sqlite 零件的 SQLITE_INIT_DDL_FILE env(开库自动应用)建库设计(实测 75s/次)+ 跨会话 schema 漂移
产出模板知识包里的模板文档(见知识包·四)文书结构的脑内起草(实测 83s/次)
直查表口径知识包文档形态(见知识包·四)散文→结论的推导

二、自动执行的装备必须过可执行的门

预建 schema 会在运行时自动执行,所以发射前必须过双次执行门:在内存库里连续执行两遍,证明"能跑"且"幂等"——不靠肉眼检查 IF NOT EXISTS。外加词法负面清单:装备是家具不是数据,INSERT(种子数据破幂等)/DROP(重开即毁)/PRAGMA 一律不收。没过门就不发射,装配照常完成——装备是加速器,不是必需品

三、边界

  • 只对 agent 自有状态(本地 SQLite)自动预建;postgres/mysql 是客户的库,自动 DDL 是越权写操作,永不做
  • persona 里只加一条可判约束("直接使用现有表,禁止重新设计 schema"),不写建表步骤——装备给的是家具,不是操作手册。

四、判据回照

DDL 是静态文件、应用者是零件进程(装配器不在场 ✓);"禁止重新设计"任何时点可判(步骤号 ✓);产物是 equipment/init.sql + BOM 里的 equipment 记录(产物 ✓)。


方案包:FDE 的交付单元

一个 preset 是一个 agent;一次客户交付通常是几个 agent + 客户知识 + 部署参数 + 凭证清单 + 验收记录。方案包(solutions/<name>/solution.yml)把这些收进一份可 git、可版本化、可在另一台机器一键重建的清单。

一、三个动作

命令做什么
solution init起一份清单骨架(客户、版本、目录、参数、agent 列表)
solution apply按清单逐个装配,每个都过装配即验证,结果落 last-apply.json
solution handover生成交付报告 HANDOVER.md

二、交付报告从工件里长出来,不从记忆里

handover 读的是每个 preset 自己的 parts.lock.yml,汇总出:交付了哪些 agent 与各自验收结论、部署参数、待配置凭证清单、知识包(来源+版本)、供应链 BOM(每个零件的 repo@rev 或服务端点 + 许可)、以及重建命令。没有一处靠人填写——能被遗忘的东西不该出现在交付文档里

三、多租户 = 换参数换凭证,不是分叉清单

同一方案交付给第二个客户:--param 覆盖部署事实,凭证配另一套,零件与知识不变。分叉清单意味着两份要各自维护的真相,那是交付债的起点。

四、判据回照

apply 跑完即退出(运行时 ✓);清单声明"有哪些 agent、各自要什么",不含执行顺序(步骤号 ✓);产物是 solution.yml + preset 目录 + HANDOVER.md,全是静态工件(产物 ✓)。


架构优先:先出 spec,再选型补缺

顺序裁定(2026-08-22,A/B 实验证明后落地):做一个 agent 最重要的第一步是先定整体架构、列全它架构上需要什么,再去目录里选型、补缺口——而不是一上来就锚定"目录有什么"去挑。

此前单 agent 装配是选型优先:第一个也是唯一带架构含义的调用(llmMapRequirement)开口就是"从目录里挑能力",架构决策(数据模型、行为契约、界面)全挂在这一次调用上顺带产出。病灶:模型锚在"现有什么",过早宣布完成。三例复杂 agent 的 A/B 实验实测——选型优先对 HR 全家桶、科研助手、医院导诊三战三次都报"0 缺口",静默丢弃真需求(科研助手漏参考文献抽取,医院导诊漏急危重症识别、边界拒答两个安全缺口)。

现在选型前先跑一次架构 spec(deriveArchSpec,不给目录 = 不偏置):列出这个 agent 架构上需要的全部能力(通用描述)、数据模型、工作流、接口。再把这份需求清单钉进选型 prompt,逼选型逐条覆盖或标缺口、绝不静默丢,同时仍取最小覆盖集(不逼过度选型)。

分流是关键:18 项架构需求里,缺零件的进缺件工单(医院导诊的对外 API),缺行为的进 persona(安全边界),有零件的选中——每一项都有着落。集成版比裸实验更聪明:它区分"要零件"与"要行为/persona",只把前者当缺口。成本:一次廉价的快模型调用(实测 ~15s,约选型的 6%)。DSH_ASSEMBLER_ARCH_FIRST=0 可退回纯选型优先。

判据回照:架构 spec 是装配时产出的静态推理,不在会话期在场(运行时 ✓);它是"这个 agent 该长什么样",不含执行顺序(步骤号 ✓);spec 的产物落进选型与 persona(产物 ✓)。


多 agent 方案交付:一套班子,不是一个巨物

assemble 装一个 agent;assemble_solution一整套班子 + 一份交付说明书。市场战役 FDE 级实测(f01 电商运营班子)暴露:主 agent 只有单发工具时,面对"装一套四个分工 agent 的班子"只能把四份职责揉进一个 30 能力的巨型单体——多 agent 分工、共享数据、HANDOVER 全落不了地。

工具接受 agent 清单(每项 {id, requirement},当作独立 assemble 需求写),逐个走同一条装配脊柱(选型→发射→独立验收),最后从工件本身(每个 preset 的 parts.lock.yml)汇总一份 HANDOVER.md:交付的 agent 表(各自验收/零件数/缺件工单/前端 URL)、每个 agent 的职责、共享数据表、部署参数、待配置凭证、供应链 BOM(每零件 repo@版本 + 许可)。产物落 .agent-presets/_solutions/<name>/(solution.yml + HANDOVER.md)。

共享数据是班子与散兵的分界。 需求说"四个 agent 共享同一套商品/订单数据"时,sharedSchema 参数一次定义公共表(幂等 DDL),装配器建一个方案级共享库 _solutions/<name>/shared/data.db,把每个子 agent 的 SQLite 默认库都钉到它——一个 agent 写的另一个能读。各 agent 自己的 stateSchema 仍幂等补齐其专属表进同一份账。实测:三个子 agent 的 SQLITE_DEFAULT_DB 全指向同一共享库,共享表 + 各专属表并存。

判据回照:HANDOVER / solution.yml / 共享库都是静态工件(产物 ✓),装配完即产出、装配器不在会话期在场(运行时 ✓),清单声明"有哪些 agent、共享什么",不含执行顺序(步骤号 ✓)。CLI 版(scripts/solution.mjs)仍在,给不经 agent 的批处理交付用;工具版是 agent 可达的同一能力。


缺件工单:补缺口的活交给谁

分工裁定(2026-08-21,与用户共同定稿):装配脊柱——选型神谕、确定性发射、独立黑盒验收——留在装配器代码里;写缺失零件这种需要全套 harness(工具+迭代+执行)的创造性工作,交给调用方主 agent。理由:装配器的辅助 LLM 调用是裸的一问一答(无工具面、无执行环),写不出能用的零件;主 agent 有完整 coding harness,写代码天然比神谕强。

于是缺口的产物不是"装配器硬写",也不只是一段贴进对话的草案,而是工单(<preset>/gaps/NN-<id>.md):spec(缺什么)+ 真实可跑的命令序列(index 流水线,质检门在流水线里)+ 本次装配的复跑指令。主 agent 照单造件、verify 过门、register 入库,重跑 assemble 即闭环——新零件经确定性发射上桌,再过独立验收。

两条铁律:

  1. 新代码必须入库,不焊死在单台 preset。 入库 = smoke 质检门 + BOM 供应链记录 + 全体后续装配可选 + 选型账本多一条训练样本;直接改 preset 的胶水是无门、无记录、不可复用的雪花。
  2. 考官独立于写代码的人。 验收永远是装配器的黑盒探针;造件的 agent 不给自己发合格证。

判据回照:工单是静态工件(产物 ✓),装配器在主 agent 造件期间不需要活着(运行时 ✓),工单不含步骤状态、每次装配整目录重写以反映现状(步骤号 ✓)。


三条边界判据

拿不准时按顺序问:

  1. 运行时判据 —— 这功能要求装配器在会话期间活着吗?要 → 出界
  2. 步骤号判据 —— 这条约束需要知道"现在是第几步"才能判定吗?要 → 出界
  3. 产物判据 —— 这功能的产出能落成静态工件(preset / BOM / 报告 / 目录)吗?不能 → 出界

判据应用示例:

功能判据结论
多轮场景探针✓ 黑盒契约,产出是报告
状态零件约定✓ 给工位不给脚本
preset 参数化✓ 静态工件
知识包零件✓ 静态教材
凭证声明✓ 声明进 preset,注入归 harness
团队装配✓ 声明"谁存在、谁能委托谁"是组织结构
流程引擎✗ 运行时判据
轨迹步骤打分✗ 步骤号判据
运行时上下文注入✗ 运行时判据

Goodhart 防线(设计注记,2026-08-27,P3 收尾)

任何未来「按体检包自我改进」的流程,禁读 selfcheck 验收标记原文(mustInclude 的字面标记)。理由:标记一旦成为优化目标就不再是测量(Goodhart)——penguin 战役 的反面实录是"聚合数字由模型写";我们的正面纪律是聚合由代码算、标记由考官持有。 若未来确需自我改进回路,两条合法路:①只喂判定结论(PASS/FAIL + 病因摘要), 标记原文不进上下文;②标记转私有(考官侧持有,selfcheck.json 只存哈希)。 本注记不动代码;真到建自改回路那天,此段升格为闸。