做这件事的起因,是上个月我在一个有几万行代码的旧项目里做重构。AI 工作台帮我把十几个文件改了一遍,自检时看起来“都改完了”,结果构建脚本里两个硬编码路径被悄悄覆盖掉,部署到测试环境才发现。站在终端前那一刻我就想明白了:我缺的不是一个更聪明的对话入口,而是一个能看清每一步执行过程的开源工作台。
所以就有了 WorkDSH。名字里的 DSH 其实是 developer shell 的缩写,核心目标很简单——把 WorkBuddy 这类 AI 编码工作台带给我的体验,用一套完全透明、本地优先、可自己改代码的方式重新实现。
这篇文章我不打算只晒项目截图。我会把 WorkDSH 从想法到开源的完整过程讲清楚,包括架构设计、核心功能落地、Skill 系统和自定义规则怎么处理,以及发布时踩到的一堆坑。想自己做一个开源版 AI 编程工作台的人,或者正在纠结“要不要重写一套自用工具”的人,应该都能从里面拿到点能用的东西。
1. 为什么我会动手重写一个 WorkBuddy:现有工具的不可控让我很难受
先说清楚,我并不是觉得 WorkBuddy 这类产品做得不好。相反,正是它们让我意识到“AI 编程工作台”应该长什么样:不是被动回答问题的聊天框,而是一个能读代码、改文件、跑命令、记住你偏好的执行中枢。问题在于,商业化产品为了体验连贯,往往会把内部决策藏起来。你看到的是结果,看不到的是“它为什么这么干”。
我自己在真实项目里遇到三个特别具体的摩擦点,可能很多用类似工具的人也有同感。
第一个摩擦点是决策不可回放。模型说“我把 xx 函数改名了”,但它到底改了哪几处、影响哪些调用点、有没有漏掉字符串拼接的场景,如果工作台不给足过程信息,你只能靠 diff 一点一点翻。小项目还好,项目一大,这种不透明就会变成焦虑源。
第二个摩擦点是环境绑定。换一台机器、换一个账号,配置和记忆就得重新来一遍。本地模型、不同厂商的模型接口、私有知识库,这些我希望随时能换,而不是被工具锁在固定通道里。
第三个摩擦点是规则难以沉淀。很多产品或支持自定义指令,但“自定义”的范围通常只停留在提示词层面。我希望定义一个规则之后,后续所有任务都生效,包括工具调用方式、文件写入偏好、命令执行策略。这些如果不在引擎底层做,而只是塞进 prompt,等于每次都要看模型心情。
所以我给自己划了一条清晰的产品边界:WorkDSH 不做 IDE,只做 AI 执行中枢。它可以作为一个独立命令行工具跑,也可以被 VS Code、Neovim 这类外部编辑器反向调用。它的所有配置、记忆、规则、日志都是纯文件,能被 Git 跟踪,能被 diff,能回滚。这是闭源工具很难给我的安全感。
1.1 我对“AI 编程工作台”的完整理解
在讲架构之前,先把概念对齐一下。我们说的不是简单“问一句答一句”,而是一个可以连续操作环境的智能体(Agent)系统。它通常包括几个层面:
- 对话层:理解用户的目标和上下文。
- 规划层:把一个大的编程任务拆成若干小步骤。
- 工具层:真正去执行读文件、改文件、跑命令、搜索代码等操作。
- 记忆层:在跨对话场景中保留项目偏好和历史决策。
- 规则层:全局指令、临时指令、项目规则之间做优先级协调。
WorkBuddy 这类产品的价值,不在于它“能写代码”,而在于它把这些层面串成了一条可执行的流水线。我的 WorkDSH 要复刻的,正是这条流水线本身,而不是某个 IDE 的界面。
1.2 WorkDSH 的目标:透明执行,而非漂亮的自动化
我给自己定了一个验收标准:任何一个文件改动,都必须能追溯到“是哪一次模型决策、基于哪些上下文产生的”。所以在 WorkDSH 里,所有工具调用、模型输入输出、命令执行日志,都会落到.wdsh/目录。出了问题不是去猜,而是打开日志看那一轮的完整上下文。
这个设计带来的直接代价是:它不像商业产品那么“自动化”。它要求使用者对过程保持关注。但我的经验是,真正要在生产环境里用的工具,首先得让人敢用。敢用比用得爽更重要——这也是我把开源作为前提的根本原因,代码摆在那里,谁都能审计。
2. WorkDSH 的核心架构:模型路由、代理循环和沙盒三层设计
整个系统我拆成了三层:模型路由层、代理循环层、沙盒执行层。三层各管各的事,互相只通过结构化消息通信。这个分层是我从第一版乱写代码里吃了亏之后才定的。
2.1 模型路由层:一套接口兼容本地模型和云端 API
模型路由层没有做得太复杂,核心思路是“一切皆 OpenAI 兼容”。无论你用的是本地模型,还是各类提供 OpenAI 兼容接口的服务,只要配置一个provider,WorkDSH 就能接上。
// provider 配置结构 export interface ModelProvider { name: string type: 'openai-compatible' | 'local-http' baseUrl: string model: string maxContextTokens: number maxOutputTokens: number temperature?: number }这里有个细节必须提醒:不同模型的maxOutputTokens上限差别很大,而且有的模型会忽略你传的参数。我第一版直接写死max_tokens=8000,结果接一个旧模型时疯狂报错,排查了半天才发现是对面只支持 2048。所以后来我把“生成预算”和“模型实际能力”分开,模型层会先做一次能力探测,再根据预算截断上下文。
另外一个经验是:本地模型和云端 API 的延迟差异巨大。代理循环里如果对时间不敏感,容易在本地模型上等半天。所以我在请求层加了超时控制和重试逻辑,而且重试时会把整个上下文重新组装,因为有些本地服务会在中途断掉连接,重新发送同一份请求也不会从断点恢复。
2.2 代理循环:计划、工具调用、观察结果,每一步都可审计
代理循环是整个系统的发动机。我采用的不是“一次生成全部步骤”的方式,而是逐步推进,每一步由模型决策一个工具调用,执行器执行完并把结果喂回去,模型再决定下一步。
async function runAgent(ctx: AgentContext, maxSteps = 30) { for (let step = 0; step < maxSteps; step++) { const decision = await ctx.model.plan(ctx.getMessages()) if (decision.type === 'finish') break const result = await ctx.executor.run(decision.tool) const observed = truncateResult(result, 12000) ctx.messages.push({ role: 'assistant', content: decision.explanation }) ctx.messages.push({ role: 'tool', toolName: decision.tool.name, output: observed }) ctx.logGitRage(step, decision, result) // 关键:这一步的完整上下文落盘 } }这个伪代码很简单,真正的复杂点在两个地方。
第一个是“结果截断”。模型生成能力再强,也不能无限容纳工具输出。一条命令可能产生几百行日志,如果全塞回上下文,后面几步的推理质量会迅速下降。我的做法是:命令输出只保留头尾各 100 行,中间如果过长就用提示词压缩成摘要,同时在上下文中标注“此处省略 N 行”。这些省略信息不会丢,会完整写进本地日志,需要时可以人工查看。
第二个是“错误注入”。工具调用失败时,不能只说“失败”两个字,必须把可理解的错误信息带回来。比如命令退出码非零、文件不存在、权限不足,每一种情况在返回给模型时都会附带结构化错误码,让模型知道该换路还是该停下。这里我的经验是:不要尝试把错误格式化得太友好,让模型看到原始 stderr 往往更能做出正确判断。
2.3 沙盒与终端执行:权限、超时、可回滚
工作台要跑命令,这是和普通聊天工具最大的区别,也是最危险的地方。我自己的策略是“宽松默认,关键拦截”。
WorkDSH 的沙盒实现分三层:
- 路径策略:默认允许读取项目目录下的文件,写入必须限定在项目目录内,或者用户在配置里显式添加的可写路径。
- 命令策略:危险命令(比如格式化磁盘、删除根目录等)会被模式匹配拦截,但拦截只是提示,用户可以在命令行里显式允许一次或永久允许。
- 超时策略:每条命令默认 30 秒超时,超时后强制结束进程,并把“已超时”作为结果返回给模型,避免模型在那里傻等。
我遇到过的最经典问题:模型为了“确认改动”,跑了一个会卡在交互输入里的脚本,进程既不退出也不报错。如果没有超时保护,整套代理循环就挂死了。所以命令执行器里必须自带子进程管理,而且我强烈建议用独立进程组来运行命令,防止子进程继续运行。
沙盒里还有个容易被忽略的点:命令执行的工作目录。模型经常在子目录里执行命令,但忘记了上一条命令在别的地方留下了状态。所以 WorkDSH 每次执行命令时都会显式声明工作目录,并在日志里记录,回放时能看出“模型当时是在哪个目录下做的决策”。
3. 把 WorkBuddy 式体验落到具体功能:diff 编辑、跨对话记忆和目录策略
架构搭好之后,真正让工具“好用”的是几个核心功能。我挑三个最关键的展开讲。
3.1 文件编辑用 diff 而不是全量覆盖
WorkDSH 的文件写入机制不是“模型生成的内容直接覆盖目标文件”,而是先生成 diff,再应用 diff。为什么?因为模型直接输出整个文件时,很容易因上下文超出而丢掉尾部内容,或者无意识地改动无关部分。diff 模式强迫模型只交付“增量”,应用层再负责合并。
默认的写入流程是这样的:
- 模型先读取文件内容(read_file)。
- 模型生成一个统一 diff 格式的修改计划。
- 应用器用
parse-diff之类的库解析,并检查 diff 是否能干净地应用到目标文件。 - 如果冲突,返回错误让模型重新生成。
这个流程看起来多了好几步,但在真实项目里能救命。有一次模型在改配置文件时,因为目标文件在我生成 diff 之后又被另一个进程改动过,应用器直接拒绝,避免了覆盖别人刚写入的内容。这个冲突检测能力,任何做 AI 编程工作台的人都值得加上。
另外,所有应用的 diff 都会存入.wdsh/patches/,并且每次改动前自动建立一份当前文件快照。回滚时不需要 Git 介入,直接从快照恢复或反向应用 diff。对那种“改了但想撤销,又不想把整个提交重来”的场景非常管用。
3.2 系统缓存目录能不能改到 D 盘:这类配置问题的体验设计
搜索热词里有一个让我特别有共鸣的:“workbuddy 系统缓存目录能改到 D 盘吗”。这说明很多人在意的是“工具的数据到底放在哪,怎么迁移”。WorkDSH 从一开始就把这类路径设计成可配置项,而且全部集中在wdsh config命令里。
WorkDSH 的目录模型是这样的:
workdir:当前项目的工作目录,所有文件操作默认在这里。cache_root:模型缓存、临时文件的根目录,默认在系统临时目录下,可以改成任意位置。log_root:运行日志和审计记录的存放位置。memory_path:跨对话记忆数据库的位置。
在 Windows 上,把缓存目录改到 D 盘只需要一条命令:
wdsh config set cache_root 'D:/WorkDSH/cache' wdsh config set log_root 'D:/WorkDSH/logs'改完之后,WorkDSH 会经历一次“数据搬迁”流程:旧缓存目录里的有效文件会被复制到新位置,然后更新配置,最后才清理旧临时文件。为什么这么设计?因为直接改配置会导致旧记录失效,重新生成又慢又浪费。配置文件本身是纯文本 YAML,放在用户目录下,既适合 Git 管理也方便排查问题。
这个功能背后其实藏着一个通用设计原则:工具的数据位置永远应该是显式的,而不是藏在某个生态的默认目录里。用户对“自己的工作台”有迁移需求时,目录可配置比什么都重要。
3.3 跨对话记忆:不是把聊天记录存下来,而是抽取偏好
“跨对话记忆”是我花了最多心思去做的模块。最初我走偏了,以为把历史对话全部存进数据库,下一次在上下文里再翻出来就是记忆。结果上下文爆掉,而且历史里最关键的偏好信息反而被淹没。
后来我把记忆设计成“抽取式”的。每当一次任务结束,系统会做一轮总结,判断任务过程中出现了哪些可复用的新信息,再写入记忆库。
记忆库用最简单的关系型结构(本地 SQLite),核心表是这样的:
CREATE TABLE memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT, content TEXT, source_session TEXT, created_at TEXT, updated_at TEXT, hit_count INTEGER DEFAULT 0 );记忆有两种类型。一种是结构化配置记忆,比如“这个项目测试命令用pnpm test,不要用npm test”“缓存目录已改到 D:/WorkDSH/cache”。另一种是自然语言经验记忆,比如“线上环境签名密钥在构建时自动注入,本地调试务必跳过签名步骤”。
加载记忆时,不是把全部 content 塞进上下文,而是做一次轻量检索。默认的检索方式是基于关键词打分,如果你愿意,也可以接入 embedding 模型做向量检索。每一条记忆只保留 key 和一段不超过 200 字的内容摘要,太多就丢。这个“摘要化”策略保证记忆在长时间使用后依然能塞进上下文窗口。
我自己的实测效果是:跨对话记忆一旦生效,最明显的感受不是“它记住了我说过的话”,而是“它不再反复问我已经回答过的问题”。那种体验飞跃,远大于单纯提升模型能力。
4. Skill 系统和自定义指令:把工作流沉淀成可复用资产
如果说记忆让 WorkDSH 变得“懂你”,那 Skill 系统就是让 WorkDSH 变得“能干”。WorkBuddy 的一大批搜索热词都是关于 skill、使用教程、自定义指令的,说明大家真正关心的是“怎么把一个常见任务固化成固定套路”。
4.1 为什么 Skill 用 JSON Schema 声明,而不是硬编码函数
我做第一版 Skill 系统时,用的是函数注册制:每种技能写一个 TS 函数,往里传上下文。后来发现这个方案很难维护——技能越加越多,执行逻辑和模型上下文耦合严重,用户想新增一个技能还得重新编译。
所以第二版我改成 JSON Schema 声明式。一个技能就是一份 JSON 文件,描述名称、触发条件、执行步骤、需要哪些上下文。执行引擎只负责解释这份 JSON,不关心具体逻辑。
下面是我实际用的一个 code_review 技能示例:
{ "name": "code_review", "description": "对指定目录做一轮代码审查,输出问题清单和修改建议", "triggers": ["review", "审查", "code review"], "steps": [ { "tool": "glob", "args": ["**/*.{ts,js}"] }, { "tool": "read_file", "target": "fileList" }, { "tool": "run_command", "cmd": "git diff --cached", "optional": true } ], "output": { "format": "markdown", "sections": ["问题清单", "风险等级", "修改建议"] } }这个 JSON 不是为了给模型看的,而是给引擎看的。引擎拿到 skill 后,会把用户的真实意图和 JSON 里的 description 一起送给模型,让模型决定如何编排 steps。换句话说,JSON 定义的是“可能性框架”,模型在框架内做具体决策。这样既保证了技能的可预测性,又保留了模型的灵活性。
技能存放位置同样遵循“一切皆文件”原则:~/.wdsh/skills/放全局技能,项目根目录下的.wdsh/skills/放项目专属技能。同名技能里项目级优先,这养成了一个很自然的“全局通用 vs 项目特化”的层次。
4.2 规则引擎:全局规则、临时指令、Skill 默认值如何共存
搜索热词里有一句话很关键:“给 WorkBuddy 定几条规则,后续对所有任务都生效”。这句话点出了规则系统和普通对话提示的根本区别。规则不是聊天的临时上下文,而是一个独立的、持久化的、有优先级的配置层。
WorkDSH 的规则分三级:
- 全局规则:写在
~/.wdsh/rules.md,对所有项目生效。 - 项目规则:写在
<project>/.wdsh/rules.md,只对当前项目生效。 - 会话指令:在交互中临时指定的指令,只影响当前会话。
运行时,引擎会把这三层规则按“会话 > 项目 > 全局”的顺序合并,越具体越靠前。但合并时不是全文拼接,而是先做 token 预算分配——每层规则先提取前 N 句关键句,总注入量不超过上下文预算的 15%。这个预算控制非常重要,否则规则一多,模型真正的推理空间就被挤压了。
我曾经因为规则写得太多,导致模型在每个工具调用前都要“背诵规则”似的绕来绕去,回答质量明显下降。后来加上关键句提取,规则命中率反而更高了。
规则文件里可以定义命令偏好、文件写入策略、避免使用的 API、强制命名规范等。它的实际威力在于配合工具执行层一起生效,而不只是影响对话语气。比如项目规则写明“所有 shell 命令必须显式设置--no-interactive”,执行器里就会在命令组装阶段强制加上这个参数。这是我没预料到但非常出效果的一个特性。
5. 从自用工具到开源项目:许可证、文档和发布检查清单
代码在自己文件夹里能跑通是一回事,开源出来有人愿意看、愿意试是另一回事。项目发布前我专门花了一整周做“开源化”改造,内容远不止把仓库设为 public 那么简单。
5.1 开源许可证怎么选:先看场景,别上来就 MIT
很多开发者第一个挑的许可证是 MIT,觉得最自由、最省事。但我的项目里有二进制分发需求,还计划后续开放插件生态,未来可能有一些品牌和商标保护方面的考量。这时 MIT 的“无保护”特性反而不适合。
我做了一个简单的对比表,把自己的需求往里套:
| 许可证 | 适用场景 | 我关心的点 |
|---|---|---|
| MIT | 只希望代码被人随便用,不在意衍生项目的约束 | 专利条款弱,商标保护不明确 |
| Apache-2.0 | 需要明确专利授权、商标声明,适合工具类项目 | 需保留修改声明,但提供专利保护 |
| GPL-3.0 | 希望所有衍生项目必须开源,阻止闭源分发 | 传染性较强,插件生态可能受限 |
最终选的是 Apache-2.0。它给我的直接好处是:代码可以被任意使用和分发,但修改后的版本需要保留原始的版权和许可声明;同时它有明确的专利授权条款,对项目未来商业化探索更友好。如果只是纯个人学习项目、不打算开放插件生态,MIT 也完全没问题,关键是想清楚“以后这个项目可能往哪走”。
另外有一个很多人忽略的点:第三方依赖的许可证兼容性。开源项目不能只给自家代码选许可,还要检查依赖项。WorkDSH 里用的一些 npm 包是 MIT,而某几个工具库是 Apache-2.0 兼容,问题不大。但如果你引用了 GPL 的组件,整个项目的许可证性质就可能被传染。发布前我专门生成了一份THIRD_PARTY_NOTICES,把依赖项许可证情况集中列出,这也是大项目审计时很容易被问到的东西。
5.2 开源仓库做这三件事,项目才不算“死码”
第一,写一份能让人 5 分钟跑起来的 README。刚开始我把 README 写成了技术架构文档,满篇都是“设计理念”。后来一个朋友告诉我:开源项目的第一读者是“想要在 10 分钟里判断值不值得装的人”。所以我改成三步式:装什么、怎么配、跑一个最小示例。尽量不放没有实操价值的套话。
第二,提供真实可运行的示例目录。我放了一个带简单 bug 的 demo 项目,README 里直接给出一条 WorkDSH 指令:“帮我找出 login.ts 里的竞态条件并修复”。新用户照着跑一遍,就能直观感受工具的实际效果,比看他写程序要强。
第三,发布前写好 issue 模板和行为准则。看起来不新鲜,但它的实际作用是提前过滤无效反馈,让用户知道“问题应该带哪些上下文”。我收到的第一条有效 issue 就是用户附上了.wdsh日志目录下的 session 记录,我几分钟就能定位问题。如果没这个模板,用户大概率只会写一句“它给我改错了”,那谁都帮不上忙。
5.3 发布后真实踩到的坑:日志膨胀、模型参数分歧、文档滞后
项目发布之后,我遇到了三个非常具体的问题,都是自用时根本意识不到的。
第一个是日志膨胀。开发时我自己用,日志目录会有意识地偶尔清一清。开源之后,别人会长时间运行,.wdsh/logs/里积累了体量很大的会话和 diff 记录。有用户反馈“跑一次任务磁盘涨了几百兆”。我后来给日志系统增加了滚动策略:只保留最近 20 次会话的完整日志,更早的只留摘要。这个问题不及时处理,开源项目的口碑会很快被消耗掉。
第二个是模型参数分歧。社区里有人用更强的模型跑同一个任务,效果很好;也有人用轻量模型跑,完全无法收敛。这说明我在模型层设定的推理参数太依赖单一模型的特性。后续我改成了“按 provider 提供默认参数的配置模板”,不同模型各自适配,而不是全局一刀切。
第三个是文档滞后于功能。我发版之后才发现 README 里写的配置项名称和实际不一致,原因是开发过程中字段名改过一版,忘了同步文档。后来我写了一个小脚本,直接从代码里的配置 schema 生成配置文档,杜绝了这类手写文档与代码脱节的问题。
6. 现在还不满意的地方和接下来的迭代想法
说点实话。WorkDSH 现在离“开源版 WorkBuddy”这个目标还差得远,尤其是我自己天天用,反而最清楚它不顺手的地方。
首先是最难处理的上下文压缩。当任务跨了很多文件、涉及大量 git 历史时,模型上下文长度还是很容易吃满。我目前是粗暴地靠“结果截断 + 记忆摘要”撑过去,但真正优雅的方案应该是做分层上下文:核心文件保持完整,边缘文件做摘要,按需再展开。这个方向我会继续迭代。
其次是多代理协作。现在的 WorkDSH 还是一个单一代理循环,读代码、规划、写代码都由同一个模型上下文完成。有些复杂任务里,负责执行改动的模型很容易被负责“读代码”的上下文污染。我下一步想做的是拆成三个角色:探查者负责搜代码,规划者负责定方案,执行者负责落改动。各自有独立的消息上下文,只通过任务交接区通信。这个架构改动很大,但我觉得值得试。
最后是插件化。我自己写了很多针对不同语言项目的技能,但每个技能都硬编码在仓库里,别人用起来未必合适。理想状态是提供一个 SDK,让第三方能注册自定义工具和自定义执行策略。WorkDSH 的工具层结构已经很接近这个目标了,缺的只是把接口正式公布出来。
如果看到这里,你也想自己做一个类似的 AI 编程工作台,我的建议是从最让你“不舒服”的那个场景开始。别一开始就想着做全套,选一个你每天都会遇到、又觉得现有工具不够透明的小场景,把它做扎实。我就是从一个“AI 改配置文件不看 diff”的愤怒下午开始的,做到现在,WorkDSH 已经成了我日常开发逃不开的一部分。它不完美,但每一行代码都在我掌控里,这种确定性,是商业工具暂时给不了我的。