1.1 需求与用例:3 个核心用例与验收标准
卷一结束时,我们手里有一个能跑的 Agent 引擎 minagent:消息协议、工具注册、朴素循环、停止条件、事件流、会话与检查点,全都写通了。但一个引擎不等于一个产品。引擎回答的是「怎么让模型和工具循环起来」,产品要回答的是「谁在什么时候用它、它必须做到什么程度才算合格」。这一节是《AI Agent 工程实战》的起点,我们把这两个问题用用例 + 验收标准的方式钉死,后面 17 章都围绕它展开。
本节把 ResearchCopilot 推进到「有明确范围」:定下 3 个核心用例、给每个用例写可判真假的验收标准、划出明确不做的三件事,并给出用例到后续章节的映射表。
1.1.1 为什么先写用例,而不是先写代码
工程实战最常见的失败不是「代码写不出来」,而是「写了一大堆,最后说不清到底做完了没有」。一个典型的对话是这样的:
- 产品:「做个 AI 研究助手。」
- 工程:「好的,支持多轮对话、联网搜索、写报告。」
- 三周后:「效果怎么样?」——「感觉还行。」
「感觉还行」不是验收标准,它是一个无法判真假的句子。它导致两个后果:一是范围失控,每加一个功能都「顺手」,项目永远收不了尾;二是无法回归,改了一处代码,没人知道是变好了还是变差了。
用例(use case)与验收标准(acceptance criteria)解决的就是这两个问题。用例回答「谁、在什么场景、要达成什么目标」;验收标准回答「怎么算达成」。两者合起来,把一句模糊的「做个研究助手」翻译成一组可以写进测试的断言。
这也正是卷一反复强调的「确定性」在工程层的延续:卷一把模型行为抽成可断言的 FakeModel,本卷把需求抽成可断言的验收标准。前者让循环可回归,后者让产品可回归。
1.1.2 三个核心用例
ResearchCopilot 是一个「研究助理」:用户提问,Agent 多跳检索本地语料、调用工具(含本地 Postgres 查询)、生成带引用的报告;后台 worker 做周期 digest。我们只做三件事,每一件都对应一类真实用户动作。
| 编号 | 用例 | 一句话目标 | 谁在用 |
|---|---|---|---|
| UC1 | 单跳事实问答 | 一次检索就能回答的确定性问题 | 日常使用者 |
| UC2 | 多跳研究并输出报告 | 需要跨多份材料综合的研究问题 | 研究员 / 分析师 |
| UC3 | 后台周期 digest | 定时把新增语料汇总成摘要 | 系统自身 |
三者的关系是递进的:UC1 是最短路径,验证「检索 + 回答」这条主链;UC2 在 UC1 之上加多跳、加工具、加报告组装;UC3 把 UC2 的能力从「用户触发」搬到「定时触发」。先做 UC1 打底,UC2 才有地基,UC3 才有可复用的报告生成器。
1.1.3 每个用例的输入、输出与失败模式
一个用例要能被实现,光有一句话目标还不够,还得写清三件事:输入长什么样、输出长什么样、什么情况算失败。三者写全,用例才算「可交付」——因为失败模式直接决定了要写哪些护栏(第 15 章)。
UC1 单跳事实问答
| 项 | 内容 |
|---|---|
| 输入 | 一个自然语言问题,例如「ResearchCopilot 用的 Postgres 是哪个版本?」 |
| 输出 | 一段简短回答 + 至少 1 条 chunk 级引用 |
| 成功 | 答案正确,且引用能回溯到语料里的真实片段 |
| 失败模式 | ① 语料里根本没有,却编了个答案(幻觉);② 答对了却没给引用;③ 引用指向不存在的 chunk |
UC1 是三条里最短的路径,但它的失败模式最典型:幻觉。注意失败模式 ① 和 ③ 的区别——前者是「内容错」,后者是「引用错」。很多系统只防前者,结果回答是对的、引用却是编的,用户一查就露馅。所以 UC1 的验收里「引用必须可回溯」和「答案必须正确」是两条独立标准。
UC2 多跳研究并输出带引用报告
| 项 | 内容 |
|---|---|
| 输入 | 一个需要综合多份材料的研究问题,例如「对比 A、B 两个方案在延迟上的取舍」 |
| 输出 | 一份结构化报告:结论 + 分点论证 + 每点的引用 |
| 成功 | 报告里的每条论断都能回链到引用;总步数不超过预算 |
| 失败模式 | ① 只查了一次就下结论(退化成 UC1);② 报告有论断没引用;③ 步数爆预算被 max_steps 截断 |
UC2 的关键失败模式是 ①——退化成 UC1。模型偷懒只检索一次就写报告,表面看「有输出」,实际没做到「多跳」。所以 UC2 的验收里有一条硬指标「至少 2 次检索」,它是用来逼出多跳的,而不是走过场。这也正说明验收标准为什么要量化:如果只写「要做多跳研究」,模型退化成单跳时你根本发现不了。
UC3 后台周期 digest
| 项 | 内容 |
|---|---|
| 输入 | 无用户输入,由定时器触发;隐含输入是「近 24 小时新增语料」 |
| 输出 | 一份摘要 + 落库的 digest 记录 |
| 成功 | 按时触发、覆盖窗口完整、重复触发不产生重复 digest |
| 失败模式 | ① 漏触发(定时器挂掉);② 重复触发导致同一窗口两份 digest;③ 生成失败后无重试 |
UC3 和前两个最大的不同是没有用户盯着。前两个用例出错,用户当场能看到;UC3 出错是静默的——digest 没生成,没人知道。所以它的验收标准里「成功率 ≥ 99%」和「失败可重试 3 次」不是锦上添花,而是唯一的观测手段:没有这两条,你连「digest 是不是在工作」都答不上来。
三个用例的失败模式汇总起来,正好对应第 15 章要建的三类护栏:
| 用例 | 主要失败模式 | 对应护栏 |
|---|---|---|
| UC1 | 幻觉、引用造假 | 输出护栏(15.2) |
| UC2 | 退化单跳、无引用 | 评测门禁(12)、输出护栏(15.2) |
| UC3 | 静默漏触发、重复 | 可观测 + 告警(13) |
这张表说明了「失败模式」为什么必须写进需求:它直接告诉你后面要建什么护栏。需求阶段偷懒不写,护栏阶段就会漏项。
1.1.4 验收标准必须能被判真假
给每个用例写验收标准时,规则只有一条:每一条标准都必须带一个可判定的阈值或明确的布尔条件。凡是「效果好」「响应快」「尽量准确」这类词,一律不允许进入标准列表。
把这条规则写成代码,比写在文档里更管用——它可以在 CI 里跑,防止有人在需求评审时又塞进一条模糊标准:
from __future__ import annotations
from dataclasses import dataclass, field
@dataclass
class UseCase:
uid: str
name: str
criteria: list[str]
chapters: list[str] = field(default_factory=list)
def is_verifiable(text: str) -> bool:
"""验收标准必须带可判定的阈值(数字)或明确的布尔条件。"""
return any(ch.isdigit() for ch in text)
CASES: list[UseCase] = [
UseCase(
uid="UC1",
name="单跳事实问答",
criteria=[
"命中语料时首字延迟 P50 < 1.2s、P95 < 3.0s",
"每条回答至少带 1 条 chunk 级引用",
"语料无命中时 100% 回复固定文案,编造率 0",
],
chapters=["3.1", "3.2", "6.1", "15.2"],
),
UseCase(
uid="UC2",
name="多跳研究并输出带引用报告",
criteria=[
"一次 run 至少触发 2 次检索、1 次 Postgres 查询",
"报告内每条论断都能回链到 >= 1 条引用",
"端到端 P95 < 45s,失败率 < 2%",
],
chapters=["3.1", "5.3", "8.1", "11.2", "15.2"],
),
UseCase(
uid="UC3",
name="后台周期 digest",
criteria=[
"每 6 小时触发一次,单次覆盖近 24 小时新增语料",
"digest 生成成功率 >= 99%,失败可重试 3 次",
"同一窗口重复触发 0 次产生重复 digest(幂等键去重)",
],
chapters=["7.3", "7.2", "6.3"],
),
]
for c in CASES:
bad = [t for t in c.criteria if not is_verifiable(t)]
print(f"{c.uid} {c.name}: {len(c.criteria)} 条验收标准, 不可判定 {len(bad)}, 章节 {','.join(c.chapters)}")
assert not bad, f"{c.uid} 存在不可判定的标准: {bad}"
print("全部用例的验收标准均可判定:通过")
用 venv 里的 Python 3.14.6 跑出来:
UC1 单跳事实问答: 3 条验收标准, 不可判定 0, 章节 3.1,3.2,6.1,15.2
UC2 多跳研究并输出带引用报告: 3 条验收标准, 不可判定 0, 章节 3.1,5.3,8.1,11.2,15.2
UC3 后台周期 digest: 3 条验收标准, 不可判定 0, 章节 7.3,7.2,6.3
全部用例的验收标准均可判定:通过
这个 is_verifiable 检查有个真实的副作用:它一开始确实把 UC1 的「语料无命中时回复固定文案,不编造事实」判成了不可判定——因为那句话里一个数字都没有。这不是检查太严,而是那句话本来就不够硬:什么叫「不编造」?后来改成「编造率 0」,它才变成一条能写进评测集的指标。检查器逼着我们把话说清楚,这正是它存在的意义。
1.1.5 明确不做:三条边界
范围失控的根源往往不是「加了什么」,而是「没说清不加什么」。ResearchCopilot 明确不做三件事,遇到诱惑时直接引用这一节。
| 不做 | 为什么不做 | 想要的人该去哪 |
|---|---|---|
| 通用聊天 | 聊天是另一个产品,会和「带引用研究」的目标打架 | 自己接模型 SDK 即可 |
| MCP Server 实现 | 本卷只做 MCP 的 client(把 MCP 当工具源) | 站内 /posts/mcp/ 专题 |
| 多租户计费 | 计费是商业系统,和 Agent 工程正交 | 独立的账单服务 |
第一条尤其重要。一个「既能闲聊、又能查资料」的助手,会在提示词、上下文预算、评测口径上同时承受两套需求的拉扯,最后两边都做不好。先做窄,再做深,是这一卷贯穿始终的取舍。
第二条的边界来自分工:本卷第 10.1 节只写「作 client 把 MCP 当工具源」,协议规范、Server 实现、传输与 OAuth 一律不碰,需要时直接链到 /posts/mcp/。第三条的边界来自正交性:租户隔离我们做(第 4.2 节),但计费不做,因为它是账单系统的事,不是 Agent 服务的事。
1.1.6 用例到章节的映射
定完用例和边界,最后一步是把它们落到章节。这张表是全书的路标:读到任何一章,都能知道它在实现哪个用例的哪一部分。
| 用例 | 主要实现章节 | 涉及的横切能力 |
|---|---|---|
| UC1 单跳问答 | 3.1 REST、3.2 SSE、6.1 Redis 缓存、15.2 输出护栏 | 配置(2)、鉴权(4)、评测(12) |
| UC2 多跳报告 | 3.1 REST、5.3 查询优化、8.1 引擎并发、11.2 记忆注入、15.2 输出护栏 | 队列(7)、可观测(13)、成本(14) |
| UC3 周期 digest | 7.3 定时任务、7.2 重试死信、6.3 限流配额 | 存储(5)、发布(16/17) |
| 三者共用 | 1.3 仓库与 CI、2.1 分层配置、10.1 MCP 客户端 | 容器化(16)、CI/CD(17) |
读表的方法:竖着看是一个用例从接口到护栏的完整链路;横着看是一类横切能力(配置、鉴权、评测……)如何服务所有用例。本卷的章节顺序大致按「先立骨架(1–4)、再补数据与队列(5–8)、再做前端与 MCP(9–10)、然后记忆与评测(11–12)、最后可观测/成本/安全/发布(13–18)」推进,这张表就是那条推进线的另一种读法。
1.1.7 反例:哪些请求不该被受理
「不做什么」划的是功能边界,这里再划一条运行时边界:有些请求虽然形式上落在三个用例里,却应该被明确拒绝或降级,而不是硬着头皮处理。把这些反例写出来,能避免后面在 API 层(第 3 章)反复纠结。
| 请求 | 处理方式 | 理由 |
|---|---|---|
| 空问题 / 纯空白 | 400 拒绝 | 无信息量,浪费一次 run |
| 超过 4000 字符的问题 | 拒绝并提示拆分 | 上下文预算(第 2 章)扛不住 |
| 要求「帮我写代码 / 翻译」 | 拒绝,回固定文案 | 超出「研究助理」范围 |
| 问语料里没有的事 | 回复「未找到」,不给编造 | UC1 失败模式 ① 的护栏 |
| 同一幂等键重复提交 | 返回既有 run_id | 避免重复计费与重复跑 |
最后一行是幂等:用户网络抖动重发同一个请求,不该跑两次。这条在第 3.3 节会用一个幂等键实现。前四行则属于「输入护栏」,第 15.1 节会讲怎么在入口处拦下提示注入与越界请求。
这些反例的意义在于:它们让「拒绝」变成需求的一部分,而不是 bug。一个从不拒绝的助手,会在每一条反例上消耗真实的 token 与算力;而明确写下的拒绝规则,本身就是省钱的护栏。
1.1.8 真实环境下的验收会怎样
上面 9 条标准都是「可判定」的,但可判定不等于好测。真实环境里,它们会分成三类:
- 能直接写成断言的:比如「至少 2 次检索」「重试 3 次」。这些用卷一的
FakeModel+ 录制夹具就能在 CI 里跑,第 12 章会搭这套评测集。 - 要压测才能测的:比如「P95 < 45s」「失败率 < 2%」。这些要真跑并发,第 14.3 节的吞吐压测正是为此。
- 要人工/评测模型判的:比如「每条论断都能回链到引用」。这类要用评分器,第 12.1 节会给出具体做法。
所以验收标准不是「写下来就完事」,它决定了后面要建哪些测试设施。这也是为什么把用例放在第 1 章:它决定了整卷的技术栈要往哪投。
把三类标准放一起看,会发现一个规律:越靠近「用户能感知的指标」(延迟、失败率),越依赖真跑;越靠近「内容质量」(引用是否成立),越依赖评测。前者要压测机与并发夹具,后者要评分器与评测集。本卷把评测(第 12 章)和压测(第 14.3 节)都放在后半程,正是因为它们是前 11 章攒下来的验收标准的「兑现工具」——需求先立,工具后到。
还有一点值得提醒:验收标准是会被修改的。跑起来之后你会发现「P95 < 45s」太松或太紧,这时候应该改标准,而不是假装它一直是对的。但无论怎么改,规则不变——改完的标准仍然必须可判定。这正是把 is_verifiable 写成代码的价值:标准可以变,标准「必须可判定」这条纪律不能变。
本节的自检清单:
- 3 个用例都能用一句话说清目标,且互不重叠
- 9 条验收标准每条都带数字或布尔条件(
is_verifiable全过) - 三条「不做」边界写明,且给出了替代去处
- 每个用例都能在映射表里找到落地的章节
- 每个用例的输入 / 输出 / 失败模式都已写明
- 反例请求的处理方式(拒绝 / 降级 / 幂等)已明确
需求定完,下一步是把它翻译成结构。下一节我们画 ResearchCopilot 的四层架构,说明引擎、服务、worker、存储各自管什么、不该管什么。想回看引擎契约本身,翻卷一的 3.1 朴素循环 。
阅读导航:上一节:目录 · 下一节:1.2 架构总览:引擎 / 服务 / worker / 存储边界 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。