学了 Drogon、FastAPI 和 FAISS,项目进度为什么仍可能是 0?用证据管理 AI 开发

一个 AI 项目的进度表很容易写成这样:

1
2
3
4
5
6
7
Drogon 学习完成
FastAPI 学习完成
Celery 学习完成
Redis 学习完成
Embedding 学习完成

项目进度:60%

但此时执行下面的验收,可能一个也通过不了:

1
2
3
4
5
能否启动 Gateway?
能否上传一份文档?
能否看到任务从 queued 进入 succeeded?
能否用固定问题召回正确 chunk?
能否返回带来源的最终回答?

“学过五个组件”和“产品完成 60%”之间没有可计算的关系。前者描述活动(activity),后者应该描述可观察结果(outcome)。如果进度只根据阅读量、代码行数或主观感觉更新,团队会在集成阶段才发现:每个组件都懂一些,但没有一条用户路径可以运行。

本文以本地 RAG 项目为例,解决一个经常被忽视的问题:怎样把学习记录转换成可验证的工程进度?我们会先诚实盘点现有证据,再写一个零依赖进度检查器,最后设计从服务骨架到 RAG 闭环的里程碑门禁。目标不是增加一套项目管理仪式,而是让“已完成”成为一个任何人都能复验的技术结论。

1. 为什么“做了很多事”仍然无法回答做到哪了?

先区分四类经常混在一起的信息:

类型 例子 能否直接证明功能完成
学习活动 阅读 Celery 文档、整理 FAISS 笔记 不能
设计产物 架构图、接口草案、数据库 ER 图 只能证明设计存在
实现产物 可构建代码、迁移脚本、容器配置 还需要运行验证
验收证据 命令、退出码、关键输出、测试报告 可以证明它覆盖的具体要求

学习与设计并非没有价值。它们降低后续决策成本,却不能替代运行结果。例如,写完一篇 Celery 状态笔记,不代表 broker 已配置正确;创建 compose.yaml,也不代表 MySQL ready 后应用一定能连接。

这也是“完成百分比”容易失真的原因。假设 60% 的分母是什么?是文件数量、工时、API 数量,还是端到端验收项?只要分母没有定义,数字便无法比较,也无法预测剩余风险。

更可靠的状态只需要三类:

1
2
3
not_started   尚无通过证据
in_progress 部分检查通过,但 milestone 尚未闭合
done 所有完成条件都有可复验的通过证据

如果确实被外部条件卡住,可以额外记录 blocked,但要写清阻塞条件和解除条件,不能把“不知道下一步怎么做”长期包装成 blocked。

2. 用现有笔记盘点,当前到底能证明什么?

仅以当前笔记目录为证据,可以确认两类高价值工作已经存在。

第一,系统目标和双服务路线已经形成。整体交付边界、垂直切片和八周计划记录在 deep-research-report.md 中。

第二,关键技术已经完成第一轮学习。相关材料包括:

方向 已有笔记 它能证明什么
C++ Web 入口 Drogon.md 已整理框架概念和常见接口
Python API FasthAPI.md 已覆盖请求、异步和上传等知识
后台任务 Celery.md 已理解队列、worker、状态和重试
数据与缓存 MySQL.mdRedis.md 已整理持久化、缓存和 TTL 等基础
检索 FAISS.mdChunk学习笔记.md 已进入切片与向量检索专项
模型与评估 句子嵌入模型.md数据处理与数据集评估.md 已开始关注模型、数据和评测

这些证据足以把“方案与知识准备”标记为完成或完成第一轮,却不能证明工程闭环已经运行。当前笔记目录尚不能提供以下证据:

  • Drogon 与 FastAPI 的实际构建、启动输出;
  • MySQL 迁移和 Redis 连接的运行结果;
  • Celery 任务提交、worker 消费与重试记录;
  • 上传、切片、索引、检索和回答的端到端测试;
  • 固定问题对应的检索指标与 bad case;
  • 失败注入、断线恢复和部署复现结果。

这句话必须准确理解为“这组笔记不能证明”,而不是断言其他目录或其他机器上一定没有实现。进度审计只应根据可见证据下结论,既不能虚构完成,也不能把未收集证据等同于功能必然不存在。

由此得到的合理阶段判断是:设计和技术储备已有,下一道门不是继续扩展学习面,而是产生第一个可重复运行的 walking skeleton(行走骨架)——两个服务能启动、互相调用,并由一条测试命令验证。

3. “完成”需要哪些证据,才不是一句口头状态?

一个验收项至少要记录五个字段:

1
2
3
4
5
claim       声称完成了什么
command 怎样复验
exit_code 命令是否成功结束
observed 实际观察到什么关键结果
artifact 结果或报告保存在哪里

例如,下面的记录比“FastAPI 已完成”更有用:

1
2
3
4
5
claim: FastAPI readiness 可用
command: curl --fail http://127.0.0.1:8000/internal/ready
exit_code: 0
observed: HTTP 200,响应包含 mysql=ok、redis=ok
artifact: artifacts/m1/readiness.txt

证据必须和 claim 匹配。一次 /health 返回 200 只能证明这个检查覆盖的存活或就绪条件,不能顺便证明文档上传和 RAG 都正常。单元测试通过也不能替代跨服务 E2E;反过来,一次 E2E 成功也不一定覆盖切片边界和错误处理。

什么是好的 Definition of Done?

Definition of Done(完成定义)应当在实现前写成布尔条件:

1
2
3
4
5
6
7
Milestone:文档 ingest 闭环

[ ] POST /v1/documents 返回 202、document_id、job_id
[ ] worker 能把固定 Markdown 切成预期数量的 chunk
[ ] jobs 最终进入 succeeded 或有结构化 failed
[ ] 重复提交同一 job 不产生重复 chunk
[ ] 进程重启后仍能查询最终业务状态

五项全部通过才能标记 done。完成三项不是“done 60%”,而是 in_progress,3/5 gates passed。这样的表达保留了真实分母,也能直接指出剩余工作。

4. 最小可运行版本:让脚本根据证据计算状态

下面的 Python 3.9+ 程序只使用标准库。它把每个 milestone 拆成 checks;只有退出码为 0、实际观察和证据位置都非空的 check 才算通过。状态由程序计算,不手工填写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
from dataclasses import dataclass
from typing import Optional

@dataclass(frozen=True)
class Check:
claim: str
command: str
exit_code: Optional[int]
observed: str
artifact: str

def passed(self) -> bool:
return (
self.exit_code == 0
and bool(self.observed.strip())
and bool(self.artifact.strip())
)

@dataclass(frozen=True)
class Milestone:
name: str
checks: tuple[Check, ...]

def result(self) -> tuple[str, int, int]:
passed = sum(check.passed() for check in self.checks)
total = len(self.checks)
if total > 0 and passed == total:
status = "done"
elif passed > 0:
status = "in_progress"
else:
status = "not_started"
return status, passed, total

def verified(claim: str, command: str, observed: str, artifact: str) -> Check:
return Check(claim, command, 0, observed, artifact)

def missing(claim: str, command: str) -> Check:
return Check(claim, command, None, "", "")

def main() -> None:
milestones = (
Milestone(
"architecture_contract",
(
verified(
"external API contract reviewed",
"python3 scripts/check_contract.py",
"6 endpoints passed schema checks",
"artifacts/m0/contract.txt",
),
verified(
"MVP scope has explicit P0 and exclusions",
"python3 scripts/check_scope.py",
"P0=7, excluded=6",
"artifacts/m0/scope.txt",
),
),
),
Milestone(
"service_skeleton",
(
verified(
"Gateway health responds",
"curl --fail http://127.0.0.1:8080/health",
"HTTP 200",
"artifacts/m1/gateway-health.txt",
),
missing(
"RAG API readiness responds",
"curl --fail http://127.0.0.1:8000/internal/ready",
),
missing(
"Gateway can call RAG API",
"python3 scripts/e2e_service_skeleton.py",
),
),
),
Milestone(
"rag_vertical_slice",
(
missing("fixture document ingested", "python3 scripts/e2e_ingest.py"),
missing("expected chunk retrieved", "python3 scripts/e2e_retrieve.py"),
missing("answer contains citation", "python3 scripts/e2e_answer.py"),
),
),
)

for milestone in milestones:
status, passed, total = milestone.result()
print(
f"{milestone.name}: status={status} "
f"checks={passed}/{total}"
)

if __name__ == "__main__":
main()

保存为 progress_check.py 并运行:

1
python3 progress_check.py

预期输出:

1
2
3
architecture_contract: status=done checks=2/2
service_skeleton: status=in_progress checks=1/3
rag_vertical_slice: status=not_started checks=0/3

脚本故意不接受一个手填的 status="done"。状态只能由 checks 推导。真实项目可以把 records 放进 JSON/YAML,或者直接从测试报告生成;但在团队规模很小时,不必先开发复杂平台,一份可读数据加一个几十行检查器已经足够。

这个最小版本仍有明确边界:非空 artifact 只是证据索引,不代表文件一定存在或没有被篡改。工程版应检查路径、时间、测试报告格式、当前 commit 和运行环境,CI 还应直接运行 command,而不是完全相信历史退出码。

5. 这段代码怎样防止“主观完成”?

Check.passed() 同时要求成功退出、观察结果和产物位置。只记录命令但没运行,exit_codeNone;命令成功却没保存关键结果,也不能作为长期证据。

Milestone.result() 不做加权平均。三项检查通过一项时,它显示 in_progress 1/3,不会给出一个看似精确却没有决策价值的 33%。这能让开发者直接看到 gate 数量,但不暗示剩余两项和第一项工时相同。

示例还揭示一个重要事实:架构 contract 可以 done,而服务骨架仍 in progress,RAG 链路仍 not started。不同类型的进度可以同时存在,不应把它们相加成一个“项目总体 47%”。

若确实需要对外汇报,可报告最近可用 milestone、当前 gate 和风险:

1
2
3
4
5
6
最近可用版本:M0,接口与 P0 范围已固定
当前目标:M1,服务行走骨架
已通过:Gateway health
缺少:RAG readiness、跨服务 E2E
风险:Python 依赖尚未锁定
下一动作:先实现 /internal/ready,并保存 curl 证据

这比“完成 35%”信息密度更高,也更容易获得具体帮助。

6. RAG 项目应该设置哪些里程碑门禁?

不要按“学完 Redis、学完 FAISS”划 milestone,而要按用户可观察能力划分。

Milestone 用户或开发者得到什么 最小验收证据
M0 Contract 清楚知道系统做什么、不做什么 API schema 检查、P0/排除项
M1 Walking Skeleton C++、Python 和依赖可以通信 两个 readiness + 跨服务 E2E
M2 Ingest 文档能稳定变成 chunk 固定 fixture、状态机、幂等测试
M3 Retrieval 问题能找到证据 固定评测集、Recall@k、索引重载一致性
M4 Answer + Citation 回答带可定位来源 最终 JSON schema、引用支撑检查、拒答用例
M5 Streaming 用户逐步收到并能恢复结果 首事件时间、终止事件、断线恢复、最终快照
M6 Delivery 陌生环境可复现 Compose 启动、迁移、E2E、README、已知限制

前一 milestone 的通过测试应进入回归集。例如 M5 开始做 SSE 后,M2 的 ingest 幂等测试和 M3 的 retrieval 评测仍要运行。否则新功能可能以破坏旧闭环为代价。

当前最值得推进哪一道门?

按现有笔记能证明的范围,下一目标应是 M1,而不是继续扩展到 Ray、LoRA 或复杂 rerank:

1
2
3
4
5
1. 固定 Drogon 和 Python 的运行版本
2. 两个服务各自提供 liveness/readiness
3. Gateway 调用一个 Python 内部接口
4. 用单条 E2E 命令验证请求和 request_id 穿过两层
5. 保存命令、输出、环境和失败处理

M1 不要求 MySQL、Redis、Celery、FAISS 和 LLM 同时出现。行走骨架越小,越容易区分跨语言、构建和网络问题。依赖应在第一个需要它的业务 milestone 中加入,而不是为了架构图完整提前启动。

7. 每次开发记录什么,才对下一次有用?

开发日志不需要复述一天的所有操作。它要保存会影响后续判断的信息:目标、改变、验证、结论和下一动作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
## YYYY-MM-DD — 一句话结果

### 可验证目标
- 调用 `POST /internal/jobs/demo` 后,job 最终进入 `succeeded`

### 实际改动
- 修改:`python_rag/app/jobs.py`
- 新增:`tests/test_demo_job.py`

### 验证
- 命令:`python -m pytest tests/test_demo_job.py -q`
- 结果:`3 passed`,exit code 0
- 证据:`artifacts/m1/demo-job-test.txt`

### 失败与判断
- 第一次 worker 未注册 task;原因是 autodiscover 路径错误
- 修正后重启 worker,通过测试

### 决策
- 业务 job ID 与 Celery task ID 分离
- 原因:外部 API 不依赖执行器内部标识

### 下一动作
- 为 worker 进程退出增加 failed/retry 集成测试

这个模板有几个刻意约束。

“今日目标”必须是可验证结果,不写“继续学习 Celery”。“实际改动”只列影响范围,不粘贴整段 diff。“验证”同时保存命令和结果,失败尝试只有在产生新判断时才记录。“下一动作”尽量只有一个最小可执行步骤,避免把完整 backlog 重复抄进每日日志。

不要在日志中记录 .env 内容、访问 token、数据库密码、私有文档正文或完整用户请求。排错证据需要脱敏;request ID 可以保留,Authorization 和 Cookie 不应保留。

8. 进度首页与开发日志为什么不能混成一个无限长文件?

读者打开进度首页,通常只想知道五件事:

1
2
3
4
5
当前可用 milestone
正在推进的 gate
最近一次通过证据
当前阻塞与解除条件
下一动作

每天的尝试、错误输出和决策过程属于时间线。全部堆在首页后,当前状态会被历史淹没;反过来只保留首页、每次覆盖旧结论,又会失去为什么这样决定的上下文。

最简单的维护方式不需要引入项目管理系统:文件顶部保留一个短快照,下面按日期追加日志;内容变长后再拆分历史归档。只有出现影响多个模块、以后很可能被重新讨论的决定时,才单独写 Architecture Decision Record(ADR)。不要为每个变量命名都创建决策文档。

快照更新时遵循一个原则:历史事实不改写,当前判断可以变化。例如原来计划第 3 周做 rerank,后来检索基线未达标而推迟,应记录“为什么推迟”,而不是把旧计划悄悄删掉,制造从未偏离计划的假象。

9. 记录进度时最常见的误区

误区一:学完框架等于模块完成

学习完成意味着具备开始实现的条件。模块完成还需要代码、运行结果、错误路径和验收证据。

误区二:用一个总体百分比概括所有风险

架构设计、模型质量和部署复现不能直接相加。报告最近完成的 milestone 和当前 gate,比没有分母的百分比更诚实。

误区三:命令执行过,但没有保存输出

几天后无法确认运行环境、退出码和关键结果,证据就难以复验。至少保存简短输出、版本和 commit;大日志可存 artifact,不要全部粘进 Markdown。

误区四:测试绿了,所以需求完成

先确认测试覆盖的要求。一个只断言 HTTP 200 的测试,不能证明 citations 正确,也不能证明任务失败时状态可恢复。

误区五:只记录成功,不记录改变方向的失败

无信息量的重复报错无需保留,但导致方案改变的失败必须记录。否则以后会重新走进同一个坑,却不知道当初为什么避开。

误区六:下一步同时列十个 P0

如果十件事都是“下一步”,实际上没有下一步。应选出能让当前 milestone 前进的最小动作,其余留在 backlog。

误区七:把环境变量和真实数据复制进日志

进度证据不是泄露凭证的理由。日志、截图和测试 fixture 都要使用脱敏或合成数据。

10. 什么时候适合这种证据式进度管理?

它特别适合跨语言、跨进程、依赖较多的 AI 工程,因为“局部完成但集成失败”是主要风险。个人项目也适合:中断一周后,证据和下一动作能快速恢复上下文。

对一次性、几十行、当场运行完就丢弃的实验,不必建立七级 milestone。只需保存实验问题、环境、命令、结果和结论。方法的复杂度应该与交付风险成比例。

证据式管理也不能替代判断。测试只能证明它覆盖的输入和环境;模型质量还需要有代表性的评测集与人工 bad-case 分析。脚本计算出的 done 是“满足当前完成定义”,不是“系统永远没有问题”。

11. 总结

学完 Drogon、FastAPI、Celery、Redis 和 FAISS,项目进度仍可能接近工程起点,因为学习活动没有自动变成可运行的用户路径。诚实的进度记录应区分知识储备、设计产物、实现产物和验收证据。

最重要的实践可以压缩为五点:

  1. 用 milestone 的布尔 gate 定义完成,不手填没有分母的百分比;
  2. 每项证据保存 claim、command、exit code、observed result 和 artifact;
  3. 只根据可见证据下结论,“没有证据”不虚构成完成,也不武断成不存在;
  4. 按用户闭环设置 M1~M6,而不是按学完多少框架划阶段;
  5. 每次日志留下一个明确的下一动作,并持续回归已经完成的 milestone。

对当前 RAG 项目,最直接的下一步是完成 M1 行走骨架:让 Drogon 与 Python 服务各自 ready,再用一条可重复的 E2E 命令证明请求确实穿过两层。等这个证据存在后,“从学明白到做出来”才真正发生。