领域驱动代理 2026:用 DDD 修复 AI 在遗留代码库中的表现

·阅读约14分钟·Evergreen Tools Team
Domain-Driven Agents

💡 工具推荐编写 .workflow.json 与术语表时,试试 Evergreen Tools 的 JSON格式化工具, JSON转CSV工具, 正则可视化工具

过去几年我重度使用 LLM 做编码和软件工程,也亲眼看过它能带来多大的生产力提升。它在新项目和小的绿色项目里表现很好。但现实是,我们日常工作里要把代理引入遗留代码库——沉重的依赖树、强耦合、堆积如山的旧债。我们很快就注意到 LLM 能交付的质量断崖式下跌。失败有一个具体的形状:在全新仓库里要一个「职位状态」字段,你得到一个;在一个已经上线四年的系统里要同样的字段,模型会发明这个概念第四种拼写——因为这个代码库从来没决定过哪个才是真的。它在该直接调用的地方写适配器,在该写适配器的地方直连。每一次失败,都是关于系统的一个问题,而系统在任何地方都没有回答过。

1. 技术债的经济学变了

软件工程一开始就只有一个主题:技术债。它是我们追求目标的自然结果。惯常的答案是把工程预算的一部分用于清理——拨出 10-20% 的技术预算解决技术债。理论上……下个季度再说。预算的五分之一是「决定改什么」和「把它敲出来」的过路费,而这两半的价格从来没有相同过。决定依然和以前一样贵;敲代码崩盘了。一个 LLM 会以不再像 2020 年的成本完成清理的机械部分——抽出的模块、跨两个包的的重构、更多测试覆盖。还技术债仍然花时间,但花的少得多,留给我的只剩「决定」的部分。

// The manifest every repo needs. Declaring the domain block
// is the ONLY registration a repo needs — no second registry
// to drift out of sync. From the job-offer-box project.
{
  "domain": {
    "project": "job-offer-box",
    "contexts": [
      {
        "name": "job-box-web",
        "docs": "CONTEXT.md",
        "subdomain": "supporting",
        "edges": [
          {
            "to": "hyperion/job-offer-backend",
            "direction": "outbound",
            "pattern": "conformist",
            "owner": "supplier"
          }
        ]
      }
    ]
  }
}

2. 战略 vs 战术:按作者划分工作

我把工作分成两半,借用 John Ousterhout《软件设计哲学》里的词——坦诚我借用了它们。他区分写代码时的两种态度:战术编程是「现在能跑就行」,战略编程是「边写边投资设计」。我用同一对词来划分作者身份,因为上面的经济学正好沿着这条线切。战略工作是决定:读系统、搞清楚什么必须变、为什么变、改动是否真的服务于功能。战术工作是把决定带进文件。前者需要系统在你脑子里;后者已经变得便宜了。在第一种里我完全参与;在第二种里我是审查者而不是实现者。我分析代码库、评估需要实现的改动及其与要交付功能的契合度,产出是每个仓库里的 GitHub issue。

Bounded contexts
// A skill is a written procedure: a markdown file of
// instructions the model loads when the task matches it.
// "Address an issue" runs the same way every time, instead
// of the way you happened to phrase it that morning.
// SKILL.md (loaded when task.type == "address-issue")
# Address a GitHub issue
1. Read CONTEXT.md for the bounded context glossary.
2. Locate every spelling of the concept in the issue.
3. If a canonical spelling exists, use it everywhere.
4. Implement, then open a PR with test coverage.

3. Skills 和子代理

issue 由我的 AI 系统基于 skills 和子代理来处理。一个 skill 是写下来的流程:一个 markdown 指令文件,任务匹配时模型加载它,于是「处理一个 issue」或「重新生成上下文地图」每次都按同样的方式运行,而不是按我那天早上碰巧的措辞。子代理是独立的模型会话,有自己的全新上下文和狭窄的工作(实现、安全审查、对照规格审查),返回一个结果而不是把整个转录倒进我的上下文。实现完成后,PR 准备好被审查。我审查、接受或要求改进,可以增量进行,关心测试覆盖和谁会坏掉:在改动落地前,我需要知道系统里哪些其他部分消费我正在碰的东西,以及它们能否承受这个改动。

// Sub-agents: separate model sessions with fresh context and
// a narrow job. They report a result back instead of dumping
// their whole transcript into your context window.
type Job = "implement" | "security-review" | "spec-review";

async function dispatchSubAgent(job: Job, issue: Issue, repo: Repo) {
  const ctx = await loadContext(repo); // CONTEXT.md + glossary
  return spawnSession({
    job,
    context: ctx,          // fresh, focused, small
    inputs: { issue, repo },
    output: "result",      // not transcript
  });
}
// Coordination stays with you; the typing is delegated.

4. DDD 作为地基

剩下的战略一半,价值完全取决于它所用的语言。这就是 DDD 出场的地方。Eric Evans 提出的方法给了我们缩小业务与技术之间沟通鸿沟的方式:基于统一语言和限界上下文的领域驱动设计,把业务需要直接翻译进技术部分。双方说同一种语言。有代理在循环里时,这条纽带更重要——它是我们向模型陈述需求、以及读懂它推理回应的方式。这就是我如此重度依赖它的原因。

Ubiquitous language

5. .workflow.json:仓库的领域宣言

我拥有的每个仓库根部都有一个 .workflow.json。它是我自己的清单,是仓库告诉我的工具链「它是什么」的地方:有哪些语言、代理应该先读哪些目录、工作落地前哪些检查必须通过。其中一块是关于领域的,声明这一块就是仓库需要的全部注册——没有第二个会漂移失步的注册表。这块命名了项目、它的限界上下文、每个上下文词汇表的位置、子域类型,以及到相邻上下文的每一条边。例子来自我的一个项目 job-offer-box:一个职位申请跟踪器,由两个仓库构成——Rust 后端和 Web 前端。

// Brownfield failure, concretely: ask for a "job offer status"
// field in a system shipping for four years and the model
// invents a fourth spelling of a concept that already exists
// three times — because the codebase never decided which one
// was real. The model guesses, and often guesses wrong.
const statusSpellings = ["job_offer_status", "offerStatus",
  "job_status", "JOFFER_STATE"]; // the model adds #4
function canonicalStatus(repo: Repo): string {
  return repo.glossary.status; // declared in CONTEXT.md
}
// The fix is not a better model. It is a decided language.

6. 让代码「准备好」被代理修改

遗留项目很深,技术深度只是第一层。下面压着第二层:混乱、缺失的意义、没有共同语言来解决它。模型跌进的就是这一层。需要升级的不是模型,是代码。代码还没有「准备好」——而准备是可以构建的,增量地、一块一块地。每个仓库声明自己的领域、上下文和词汇表;每个 issue 由有纪律的子代理处理;每次 PR 由你以审查者身份把关。当系统在任何一个地方都能回答「这是什么意思」,模型的猜测就变成了执行。这就是 2026 年让 AI 在遗留代码库里真正干活的方式。

// Strategic vs tactical authorship (after John Ousterhout).
// Deciding stayed expensive; typing collapsed. The LLM does
// the mechanical half; you keep the deciding half.
async function agentLoop(repo: Repo, backlog: Issue[]) {
  for (const issue of backlog) {
    const plan = await strategicPlan(repo, issue); // you, with context
    const pr = await tacticalImplement(repo, plan); // agent
    await reviewAsHuman(pr); // you, as reviewer not implementer
  }
}
// The economics: a fifth of the budget used to be deciding
// plus typing. Typing now costs almost nothing.

📌 常见问题 FAQ

为什么 LLM 在遗留代码库里表现这么差?

失败有具体形状:模型在系统从未回答过的问题上猜测。遗留代码库有沉重的依赖树和强耦合,而且往往没有决定过统一的概念拼写。问题不是模型需要升级,而是代码没有「准备好」被修改。

为什么 LLM 在遗留代码库里表现这么差?

失败有具体形状:模型在系统从未回答过的问题上猜测。遗留代码库有沉重的依赖树和强耦合,而且往往没有决定过统一的概念拼写。问题不是模型需要升级,而是代码没有「准备好」被修改。

为什么 LLM 在遗留代码库里表现这么差?

失败有具体形状:模型在系统从未回答过的问题上猜测。遗留代码库有沉重的依赖树和强耦合,而且往往没有决定过统一的概念拼写。问题不是模型需要升级,而是代码没有「准备好」被修改。

为什么 LLM 在遗留代码库里表现这么差?

失败有具体形状:模型在系统从未回答过的问题上猜测。遗留代码库有沉重的依赖树和强耦合,而且往往没有决定过统一的概念拼写。问题不是模型需要升级,而是代码没有「准备好」被修改。

为什么 LLM 在遗留代码库里表现这么差?

失败有具体形状:模型在系统从未回答过的问题上猜测。遗留代码库有沉重的依赖树和强耦合,而且往往没有决定过统一的概念拼写。问题不是模型需要升级,而是代码没有「准备好」被修改。

DDD 和 AI 代理有什么关系?

领域驱动设计基于统一语言和限界上下文,把业务需要直接翻译进技术部分。有代理在循环里时,它是我们向模型陈述需求、读懂模型推理的语言。DDD 就是代理和业务之间的翻译层。

DDD 和 AI 代理有什么关系?

领域驱动设计基于统一语言和限界上下文,把业务需要直接翻译进技术部分。有代理在循环里时,它是我们向模型陈述需求、读懂模型推理的语言。DDD 就是代理和业务之间的翻译层。

DDD 和 AI 代理有什么关系?

领域驱动设计基于统一语言和限界上下文,把业务需要直接翻译进技术部分。有代理在循环里时,它是我们向模型陈述需求、读懂模型推理的语言。DDD 就是代理和业务之间的翻译层。

DDD 和 AI 代理有什么关系?

领域驱动设计基于统一语言和限界上下文,把业务需要直接翻译进技术部分。有代理在循环里时,它是我们向模型陈述需求、读懂模型推理的语言。DDD 就是代理和业务之间的翻译层。

DDD 和 AI 代理有什么关系?

领域驱动设计基于统一语言和限界上下文,把业务需要直接翻译进技术部分。有代理在循环里时,它是我们向模型陈述需求、读懂模型推理的语言。DDD 就是代理和业务之间的翻译层。

.workflow.json 是什么?

仓库根部的领域清单:声明语言、代理应先读的目录、落地前必须通过的检查,以及领域块——项目名、限界上下文、词汇表位置、子域类型、到相邻上下文的边。声明它 = 注册完成。

.workflow.json 是什么?

仓库根部的领域清单:声明语言、代理应先读的目录、落地前必须通过的检查,以及领域块——项目名、限界上下文、词汇表位置、子域类型、到相邻上下文的边。声明它 = 注册完成。

.workflow.json 是什么?

仓库根部的领域清单:声明语言、代理应先读的目录、落地前必须通过的检查,以及领域块——项目名、限界上下文、词汇表位置、子域类型、到相邻上下文的边。声明它 = 注册完成。

.workflow.json 是什么?

仓库根部的领域清单:声明语言、代理应先读的目录、落地前必须通过的检查,以及领域块——项目名、限界上下文、词汇表位置、子域类型、到相邻上下文的边。声明它 = 注册完成。

.workflow.json 是什么?

仓库根部的领域清单:声明语言、代理应先读的目录、落地前必须通过的检查,以及领域块——项目名、限界上下文、词汇表位置、子域类型、到相邻上下文的边。声明它 = 注册完成。

skills 和子代理是什么?

skill 是 markdown 指令文件,任务匹配时加载,让流程每次都按同样方式运行;子代理是有独立上下文和狭窄工作(实现、安全审查、规格审查)的独立模型会话,返回结果而非倾倒转录。

skills 和子代理是什么?

skill 是 markdown 指令文件,任务匹配时加载,让流程每次都按同样方式运行;子代理是有独立上下文和狭窄工作(实现、安全审查、规格审查)的独立模型会话,返回结果而非倾倒转录。

skills 和子代理是什么?

skill 是 markdown 指令文件,任务匹配时加载,让流程每次都按同样方式运行;子代理是有独立上下文和狭窄工作(实现、安全审查、规格审查)的独立模型会话,返回结果而非倾倒转录。

skills 和子代理是什么?

skill 是 markdown 指令文件,任务匹配时加载,让流程每次都按同样方式运行;子代理是有独立上下文和狭窄工作(实现、安全审查、规格审查)的独立模型会话,返回结果而非倾倒转录。

skills 和子代理是什么?

skill 是 markdown 指令文件,任务匹配时加载,让流程每次都按同样方式运行;子代理是有独立上下文和狭窄工作(实现、安全审查、规格审查)的独立模型会话,返回结果而非倾倒转录。

怎么开始?

从小处开始:挑一个仓库,写 CONTEXT.md 词汇表,声明 .workflow.json 的领域块,把「处理 issue」变成 skill,用子代理实现、你审查。一块一块地让代码准备好。

怎么开始?

从小处开始:挑一个仓库,写 CONTEXT.md 词汇表,声明 .workflow.json 的领域块,把「处理 issue」变成 skill,用子代理实现、你审查。一块一块地让代码准备好。

怎么开始?

从小处开始:挑一个仓库,写 CONTEXT.md 词汇表,声明 .workflow.json 的领域块,把「处理 issue」变成 skill,用子代理实现、你审查。一块一块地让代码准备好。

怎么开始?

从小处开始:挑一个仓库,写 CONTEXT.md 词汇表,声明 .workflow.json 的领域块,把「处理 issue」变成 skill,用子代理实现、你审查。一块一块地让代码准备好。

怎么开始?

从小处开始:挑一个仓库,写 CONTEXT.md 词汇表,声明 .workflow.json 的领域块,把「处理 issue」变成 skill,用子代理实现、你审查。一块一块地让代码准备好。