告别AI路由器:用FDE事实清单掌控外包项目交付
2026/9/19 5:58:16 网站建设 项目流程

一个人接外包最常见的状态,不是写代码写到半夜,而是白天转发客户消息,晚上转发 AI 生成结果,最后自己变成了客户和 AI 之间的一个“路由器”。需求从客户嘴里说出来,经过你转述给 AI,AI 吐出一堆代码,你再原样交付给客户。看起来都有回应,实际上没有任何一方真正理解需求。

这不是个段子,是我自己踩过坑之后的真实感受。后来我接触到了 FDE 相关的方法论,才开始有意识地改变这种局面。这篇文章会从“为什么你会变成 AI 路由器”这个现象聊起,再结合一个人接外包的场景,讲清楚 FDE 的核心思路、完整实操流程和可复用的模板。

如果你也是一个人接外包,或者在小团队里承担“全能型选手”角色,这篇文章可能比多学一个框架更有用。

1. 什么是“AI 路由器”,以及 FDE 能解决什么问题

1.1 “AI 路由器”现象的本质

先来还原一下场景。

客户发来一段话:“帮我们做个客户管理系统,要能添加客户、能改客户信息、能导 Excel,最好界面好看一点。”

你收到消息后,打开某个 AI 编程工具,把这句原话直接粘进去,然后 AI 生成了一个项目样板。你快速跑起来,截图发给客户说“功能已经有了”。客户看了一眼说“这个字段不对,我们还要记录客户的来源渠道”,你再把这句话粘给 AI,继续改。几轮下来,项目功能越来越多,但你发现自己根本没理清楚业务,只是在机械地做信息搬运。

这类行为模式和路由器非常像:

  • 数据包从一个接口进,从另一个接口出;
  • 中间不做语义解析,不判断内容是否完整;
  • 只要链路通畅,就不关心两端的真实状态;
  • 一旦链路抖动,错误就会被放大。

当你的角色变成“需求搬运工”和“代码搬运工”时,其实已经失去了交付把控能力。客户会觉得你只是在传话, AI 工具又成了真正的开发主力,而你自己既没有积累业务理解,也没有形成工程质量判断。

1.2 FDE 到底是什么

FDE 在项目资料和团队实践中的表述并完全统一。结合多个团队的落地口径,它通常被解释为面向语意的事实方法论,英文里会和 Fact-Driven Engineering、Fact-Driven Development 等说法挂钩。

我的理解是这样的:

FDE 的核心单位不是提示词,不是代码,而是“事实”。

传统开发流程中,需求通过用户故事、原型图、PRD 文档传递,但在这个过程中,需求经常被转述变形。而 FDE 的做法是先把所有相关方共识、业务约束、验收标准拆成一条条可核对的事实,让 AI、客户、辅助工程师都在同一份事实清单上工作。

和以往写需求文档相比,区别在于:

  • 文档偏重叙述,事实清单偏重结构化;
  • 提示词偏重“让 AI 做什么”,事实清单偏重“事实是什么”;
  • 直接问 AI 得到的是生成式结果,基于事实清单得到的是可校验结果。

简单说,FDE 工程师不是把客户需求翻译给 AI,而是把客户需求加工成事实,让 AI 在事实基础上生成实现方案。这里的差距在于,你不只是让 AI 更容易理解需求,而是让你自己能真正掌握需求边界。

1.3 一个人接外包为什么更需要 FDE

一个人接外包时,需求分析、架构设计、编码、测试、部署、沟通全压在自己身上。在这种情况下,时间非常值钱,但更值钱的是“确定性”。

如果只靠对话式 AI 随机生成,每次结果都不一样,那你每次都要从头做代码走查。如果用 FDE 的方式先维护一份事实清单,AI 生成代码后,你可以把代码逐条映射回事实清单,快速判断哪一块对了、哪一块少了、哪一块越界了。这种对照能力,才是单人交付中最容易忽略的部分。

所以,掌握 FDE 并不是为了增加流程负担,而是为了用最小的成本换回对项目的控制权。

2. 环境准备与工具链

2.1 选型思路

FDE 本身是一种工作方法,不依赖某个特定软件。只要满足下面三个条件,都可以作为 FDE 落地工具:

  • 能编辑 Markdown 或 JSON 文件,用来维护事实清单;
  • 能调用 AI 对话服务或代码生成能力;
  • 能执行代码、运行测试、记录版本。

因此本文的实战环境不绑定某个商业产品。AI 编程工具你可以根据自己的习惯来选择,常见的 Cursor、GitHub Copilot、通义灵码、豆包等都可以。重点是方法,不是工具。

操作系统方面,Windows、macOS、Linux 均可以,下面示例以通用命令为主。

2.2 示例项目目录结构

实战部分会做一个小型“客户管理 API”,包含新增客户、查询客户列表、导出 CSV 三个功能。这个项目虽然不复杂,但它能完整演示 FDE 从事实清单到代码验证的闭环。

推荐按照下面的目录来组织文件:

fde-customer-api/ ├── facts/ │ ├── requirements.md │ └── traceability.md ├── app/ │ └── main.py ├── tests/ │ └── test_customers.py ├── data/ │ └── customers.db ├── .gitignore └── README.md

这个结构的核心思路是把“事实”和“实现”分开管理:

  • facts 目录放需求事实和跟踪矩阵;
  • app 目录放实际代码;
  • tests 目录放验证脚本;
  • data 目录只放本地 SQLite 数据库文件,不纳入版本管理。

2.3 基础环境说明

示例项目使用 Python 和 FastAPI,主要因为这些组件轻量、适合快速演示。如果你在自己的外包项目中使用 Java、Go 或 Node.js,FDE 的方法完全不受影响。

# 建议使用 Python 3.10 及以上版本 pip install fastapi uvicorn

以上命令假设你已经安装了 Python 和 pip。具体 Python 版本需要根据你的本地环境调整。示例代码中使用了int | None这种类型语法,它属于 PEP 604 的写法,在 Python 3.10 及以上版本中才支持。如果你使用的是 Python 3.8 或 3.9,需要把id: int | None改成id: Optional[int],并在文件头部增加from typing import Optional

3. FDE 核心方法拆解

3.1 事实的四个基本要素

一条能被 AI 和客户共同理解的事实,至少要包含四个信息:

  • 主体:这条事实是针对谁的?比如“客户管理员”“普通用户”“系统本身”;
  • 行为:期望发生什么事?比如“新增客户”“导出列表”;
  • 约束:有哪些边界条件?比如“姓名必填”“手机号 11 位”;
  • 验收标准:怎么判断做完了?做对了?

把四个要素合在一起,就是一条可执行的事实。

给出一个最小示例:

{ "fact_id": "F001", "actor": "客户管理员", "action": "新增客户", "constraints": [ "姓名为必填项", "手机号必须为 11 位数字" ], "acceptance_criteria": [ "提交成功后返回 customer id", "客户列表接口能看到新记录" ], "status": "confirmed" }

这里面最容易被忽略的是验收标准。我们平时让 AI 写代码时,经常只给功能名称,不给验收条件,比如“写一个新增客户接口”。AI 确实能写出来,但你怎么判断它写对了?如果事实清单里提前写了“提交成功后返回 customer id”,那么 AI 一旦没返回 id,你就能立刻发现问题。

3.2 事实与提示词的关系

很多人以为 FDE 就是“写更好的提示词”,其实它比提示词更底层。

提示词是“基于事实的一次性表达”,事实是“跨多轮对话的稳定上下文”。

举个例子:

  • 普通提示词:帮我写一个客户管理接口,支持增删改查,数据存 SQLite。
  • 带事实的提示词:以下是事实清单,请按照 F001 到 F004 实现代码。事实中约束必须优先满足,如果没有实现某个验收标准,请明确说明原因。

第二种写法看起来差不多,但实际效果差异很大。因为事实清单通常是一份长期维护的文档,你可以在每次对话中反复引用,不必重新输入需求背景,AI 也不会因为上下文丢失而逐渐偏离方向。

3.3 从客户原话到事实清单

外包场景中,客户的原话往往是一段很口语化的描述。真实项目里我曾收到过类似这样的需求:“客户信息总是记在 Excel 里,想做成网页,谁都能填,但是别让所有人都能删。”

这句话如果直接丢给 AI 工具,生成的系统大概率只能应付展示。但如果先加工成事实清单,就会变成:

  • F001:需要提供一个网页表单,客户可填写客户信息;
  • F002:只有管理员角色可以删除记录;
  • F003:普通用户只能查看和新增;
  • F004:原始 Excel 数据需要一次性导入系统(附加事实)。

经过这样的拆解,原本模糊的“谁都能填”“别让所有人都能删”就变成了清晰的角色权限边界。单凭这两条细节,后续设计数据库、接口权限时都能少走很多弯路。

3.4 双向追溯

事实清单不是给 AI 看过一次就完事,它还需要和代码形成双向追溯。

什么意思?

  • 正向追溯:每一条事实,都能找到对应代码实现;
  • 反向追溯:每一段代码,都能说出它满足了哪条事实。

为了落地这个思想,可以在项目里维护一份traceability.md

# 事实实现追溯表 | 事实编号 | 事实描述 | 实现文件 | 状态 | | --- | --- | --- | --- | | F001 | 新增客户 | app/main.py 中的 add_customer | 已完成 | | F002 | 查询客户列表 | app/main.py 中的 list_customers | 已完成 | | F003 | 导出 CSV | app/main.py 中的 export_customers | 已完成 | | F004 | 管理员删除客户 | 未实现 | 待开发 |

这个表看起来多了一步工作,但在一个人接外包的项目里,它反而是省时间的。交付前你不用重新读一遍全部代码,只需要对着追溯表检查“哪些事实没落地”就行了。

4. FDE 实战:一个人快速交付“客户管理 API”

下面用一个最简单但完整的外包项目来演示完整的 FDE 工作流。这次的需求只有一句话:

客户要求做一个客户信息管理系统,支持新增客户、查看客户列表、导出 CSV 文件。

4.1 先做事实清单,不要先写代码

拿到原始需求后,不要急着创建项目,也不要急着打开 AI 工具。先花 10 分钟列问题清单,找客户确认。你可以用下面这些问题进行沟通:

  • 客户信息需要包含哪些字段?
  • 新增客户时,哪些字段是必填的?
  • 导出 CSV 的列顺序是什么?
  • 是否需要登录?是否需要区分管理员?
  • 数据量大概多少?是否需要从已有 Excel 导入?

假设客户回复了关键信息,最终整理出一份事实清单:

{ "facts": [ { "fact_id": "F001", "actor": "所有用户", "action": "新增客户", "constraints": [ "姓名必填", "手机号必填且为 11 位数字", "等级默认是「普通」" ], "acceptance_criteria": [ "新增成功后返回客户 id", "列表接口可看到新记录" ] }, { "fact_id": "F002", "actor": "所有用户", "action": "查看客户列表", "constraints": [ "列表按创建时间倒序排列" ], "acceptance_criteria": [ "接口返回所有字段", "列表按创建时间倒序排列" ] }, { "fact_id": "F003", "actor": "所有用户", "action": "导出客户 CSV", "constraints": [ "使用 UTF-8 编码", "包含 id、姓名、手机号、等级、备注等字段", "Excel 打开不出现乱码" ], "acceptance_criteria": [ "导出文件可下载", "文件打开中文正常显示" ] } ] }

这份 JSON 可以直接保存到facts/requirements.md中,也可以用 Markdown 表格来写。之所以建议用 JSON,是因为后续它可以直接被脚本解析,甚至可以作为 AI 对话的上下文片段。

4.2 基于事实生成代码

事实清单确定之后,再让 AI 动手。

不要使用这样宽泛的提问:

帮我写一个客户管理系统

建议这样组织你的对话:

以下是项目事实清单(JSON 格式),请根据事实实现一个 FastAPI 项目。 事实: (在这里粘贴事实清单 JSON) 要求: 1. 使用 SQLite 存储数据,启动时自动建表。 2. 实现新增客户、查询客户列表、导出 CSV 三个接口。 3. 手机号校验规则在两个接口中都要生效。 4. 请在代码注释中标注对应的 fact_id,方便追溯。

这段表述相比“帮我写客户系统”多了两个信号:

  • 约束被明确要求强制执行;
  • 注释中标注 fact_id,形成代码和事实的映射。

这样生成的代码,你拿到手之后更容易检查,AI 也不容易“偏题”。

4.3 核心代码示例

下面是你需要落到app/main.py的最小示例。整个示例的思路是:启动时初始化数据库表,然后实现三个基础接口。

# 文件路径:app/main.py import csv import io import sqlite3 from typing import Optional from fastapi import FastAPI, HTTPException, Response from pydantic import BaseModel app = FastAPI() DB_NAME = "data/customers.db" class Customer(BaseModel): id: Optional[int] = None name: str phone: str level: str = "普通" remark: str = "" def init_db(): """启动时初始化数据库表结构。""" conn = sqlite3.connect(DB_NAME) cursor = conn.cursor() cursor.execute( """ CREATE TABLE IF NOT EXISTS customers ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, phone TEXT NOT NULL, level TEXT DEFAULT '普通', remark TEXT DEFAULT '', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) """ ) conn.commit() conn.close() def validate_customer(c: Customer): """校验客户信息,对应事实 F001 的约束。""" if not c.name or not c.name.strip(): raise HTTPException(status_code=400, detail="姓名不能为空") if not c.phone.isdigit() or len(c.phone) != 11: raise HTTPException(status_code=400, detail="手机号必须为 11 位数字") @app.post("/customers") def add_customer(c: Customer): """新增客户,对应事实 F001。""" validate_customer(c) conn = sqlite3.connect(DB_NAME) cur = conn.cursor() cur.execute( "INSERT INTO customers (name, phone, level, remark) VALUES (?, ?, ?, ?)", (c.name, c.phone, c.level, c.remark), ) conn.commit() new_id = cur.lastrowid conn.close() return {"id": new_id, "message": "客户创建成功"} @app.get("/customers") def list_customers(): """查询客户列表,按创建时间倒序,对应事实 F002。""" conn = sqlite3.connect(DB_NAME) cur = conn.cursor() cur.execute( "SELECT id, name, phone, level, remark, created_at FROM customers ORDER BY created_at DESC" ) rows = cur.fetchall() conn.close() return [ { "id": r[0], "name": r[1], "phone": r[2], "level": r[3], "remark": r[4], "created_at": r[5], } for r in rows ] @app.get("/customers/export") def export_customers(): """导出客户列表为 CSV,对应事实 F003。""" conn = sqlite3.connect(DB_NAME) cur = conn.cursor() cur.execute("SELECT id, name, phone, level, remark, created_at FROM customers") rows = cur.fetchall() conn.close() output = io.StringIO() writer = csv.writer(output) writer.writerow(["id", "name", "phone", "level", "remark", "created_at"]) writer.writerows(rows) # 加入 UTF-8 BOM,避免 Excel 打开 CSV 出现中文乱码 content = "\ufeff" + output.getvalue() return Response( content=content, media_type="text/csv; charset=utf-8", headers={"Content-Disposition": "attachment; filename=customers.csv"}, ) init_db()

这里需要说明的是,以上代码是最小可运行示例,不是完整的生产级项目。如果你要在真实外包项目中交付,还需要补充用户认证、权限控制、异常日志、分页、重复数据校验等内容。

上面的代码中,所有输入校验都集中在validate_customer函数里。这样设计的好处是,以后如果客户追加事实,比如“手机号需要支持外国号码”,只需要修改这一个函数,而不是在多个接口中分别寻找校验逻辑。

4.4 运行与验证

在项目根目录执行下面命令启动服务:

cd fde-customer-api uvicorn app.main:app --reload

启动成功后,你会看到类似下面的输出:

INFO: Uvicorn running on http://127.0.0.1:8000

然后可以用 curl 验证接口。

新增客户:

curl -X POST http://127.0.0.1:8000/customers \ -H "Content-Type: application/json" \ -d '{"name": "张三", "phone": "13900001111", "level": "VIP", "remark": "来自展会"}'

查询列表:

curl http://127.0.0.1:8000/customers

导出 CSV:

curl -O http://127.0.0.1:8000/customers/export

4.5 校验和结果说明

接口全部跑通之后,回到事实清单,把每一条事实的验收标准逐项勾选。

  • 新增客户成功后返回 id?– 满足
  • 列表接口能看到新记录?– 满足
  • 姓名和手机号为必填?– 校验逻辑已实现
  • CSV 导出为 UTF-8 编码?– 已实现
  • Excel 打开中文不乱码?– 已通过添加 BOM 解决

从客户需求到事实清单,再到代码、测试、验收,整个过程就是一次 FDE 闭环。这套闭环并不复杂,但它能保证你不会交付一个“看起来很像但细节全偏”的系统。

5. 常见问题与排查思路

在实际使用 FDE 方法时,会有几个高频问题。我整理成了表格,方便你遇到问题时快速定位。

问题现象常见原因解决思路
AI 生成的代码缺少关键校验提示词没有给出约束,只有功能名在事实清单中明确写出约束条件,并让 AI 逐条检查
客户频繁改需求,代码返工严重前期没有把需求拆成事实开工前花 10 分钟整理事实清单,请客户确认
代码能运行,但不知道每段代码在干嘛缺少追溯机制代码注释中标注 fact_id,维护 traceability.md
对话历史太长导致 AI 越来越偏上下文被无关对话污染每次新对话都重新贴事实清单,不要依赖长对话记忆
导出 CSV 在 Excel 中中文乱码缺少 UTF-8 BOM导出内容前添加\ufeff前缀
项目本地能跑,换一台机器就跑不了依赖和环境没记录使用 requirements.txt 或虚拟环境固定依赖
AI 实现了未要求的功能提示词边界不清晰,没有负面约束在事实清单中增加“不做什么”的约束区块

这些问题并不是 FDE 本身带来的,而是之前我们太依赖临时对话而产生的。当你把事实固定下来后,很多问题其实是自动消失的。

比如“客户频繁改需求”这个问题的根源,往往不是客户有多难沟通,而是你没有一个双方都能看到的“当前事实版本”。客户说“我要加一个字段”,你可以打开事实清单,明确告诉他:

“这个需求会修改 F001 的约束条件。改完之后新增客户接口会多一个字段,不影响其他接口。”

这样一来,需求变更就不再是一句口头的“行吧,我加一下”,而是变成一次有边界、有影响评估的变更记录。

6. 最佳实践与工程建议

6.1 事实优先于指令

在接外包项目时,我建议你给自己定一条规则:任何任务开始前,先写事实,再写指令。你可以用下面的顺序:

  • 先把客户原话复制到文档里;
  • 然后圈出其中的主体、动作、约束、验收;
  • 最后再生成事实清单,决定哪些事实需要交给 AI 实现。

这套流程至少能帮你过滤掉 30% 的需求理解偏差。

6.2 给 AI 的提示词要包含事实 ID

给 AI 的提示词不要只说“实现新增客户”,试着把事实 ID 带上。比如:

根据 F001 实现新增客户接口。F001 约束:姓名必填,手机号必填且为 11 位数字。

带上 fact_id 后,AI 生成代码的注释会自然携带这个标识,后续你写追溯表、做代码走查都会轻松很多。

6.3 数据库变更必须可回滚

外包项目一旦进入维护阶段,数据库结构就会开始频繁变化。无论技术栈是什么,都强烈建议先备份数据库,再执行任何升级脚本。

以本文的 SQLite 示例为例,你可以定期复制一份:

cp data/customers.db data/customers_$(date +%Y%m%d).db

如果是 MySQL、PostgreSQL 等数据库,更推荐使用专门的迁移工具管理表结构变更。不要把手工修改 SQL 当成常态。

6.4 不要一次性生成整个系统

让 AI 一次性生成一个完整系统,表面上效率很高,实际上风险很大。

理由很简单:系统越大,AI 越容易在某个模块引入隐含错误。而这些错误往往不会在启动时报错,而是在业务运行一段时间后暴露。

更合理的节奏是:

  • 一次只实现一个事实;
  • 实现后立刻验证;
  • 验证通过后,再进入下一条事实。

用本文的例子来说,应该先实现 F001 新增客户,运行测试通过后,再实现 F002、F003。这样虽然多花了两次“打开终端”的时间,但整个过程中你始终清楚代码是处于哪个阶段。

6.5 警惕把客户涉密信息写入提示词

如果外包项目涉及企业客户的数据,千万要小心。不要把客户真实数据库、真实手机号、财务数据等信息直接发给 AI 工具。建议在事实清单和提示词中使用脱敏样例数据,例如手机号统一写成13900001111这类测试数据。必要时应和客户签署数据处理协议,并明确哪些信息不允许进入第三方 AI 平台。

6.6 时间估算要基于事实而非功能点

一个人接外包报价时,如果按“功能数量”来估时间,很容易低估。因为两个看起来一样的功能,其约束数量可能完全不同。

同样是“新增客户”功能:

  • 约束为“姓名和手机号必填”时,可能只需要 1 小时;
  • 约束为“手机号需要查重、区分个人客户和企业客户、同步到 CRM”时,可能就需要 1 天。

用事实数量来估算,比用页面数量来估算更准确。你可以在事实清单里把每个事实标一个预估工作量,然后加起来,再乘以 1.3 到 1.5 的缓冲系数。

7. 总结与下一步

这篇文章从“当 AI 路由器”的痛点出发,介绍了 FDE 方法的思考方式和实战流程。核心可以概括为三句话:

先把客户口语翻译成结构化事实,再让 AI 基于事实生成实现,最后用验收标准校验事实是否落地。

你可以在自己的下一个外包项目中尝试这样做:

  • 准备一个facts/requirements.md,把客户需求拆成事实;
  • 每次与 AI 对话前,先把当前要处理的事实粘贴进去;
  • 每完成一个功能,就在追溯表中更新状态;
  • 交付前,只根据验收标准逐条检查,不看中间过程。

当你能把一段口语化的需求变成一份干净、可核对的事实清单,并且让 AI 在事实边界内工作时,你就不再是客户和 AI 之间的路由器。你会成为那个真正决定项目走向的人。

下一步,建议你从一个小型内部项目开始,按本文流程完整走一遍。先建事实清单,再写代码,再维护追溯表。不用一开始就追求大而全,把一个流程做通,再去优化细节,会比反复看方法论更有效果。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询