第 54 章 从 Notebook 到可用服务与可信的用户体验
米拉 想知道,2026-09-14 去北京出差,住宿限额是多少。第 46 章的函数已经能返回每间夜 750 元,以及支持这个答案的制度。在 Notebook 里,作者知道单元格什么时候开始运行,可以查看变量,也清楚哪个账户是虚构的。打开应用的用户看不到这些。如果页面一直没有动静,她该等下去,还是再问一次?按下“停止”之后,答案还会不会突然出现?如果金额先到、来源后到,她又该怎样判断?
算出答案,只完成了任务的一部分。我们还要保留用户原来的问题,让处理进度有意义,落实访问权限,并把结果解释清楚。本章继续已有的知识助手,默认沿用经过核验的原文摘录流程,从小型虚构资料库中组织答案。第 33 章的 Mini GPT 候选没有通过选型,第 38 章的格式修复也没有让它们成为可靠的制度助手。给模型加上 HTTP 接口,不会改变这些证据。
54.1 为请求的每一段安排责任
Section titled “54.1 为请求的每一段安排责任”先用最简单的办法:页面调用回答函数,再把返回的字符串显示出来。这可以走通一次成功请求,却没有说明谁可以问、按哪个日期回答、连接断开后怎么办。把这些决定明确下来,并不需要先搭建庞大的分布式系统。
用户界面(user interface,UI) 收集问题和适用日期,显示进度与来源。业务逻辑(business logic) 确认调用者,校验输入,检查权限,限制工作量,并决定请求的最终状态。检索(retrieval) 找到适用于问题日期的资料,提供支持结论所需的完整范围。回答/模型层(answer/model layer) 根据这些资料准备候选答案;宿主检查结构与证据后,才允许返回。这是在分配职责,不是在规定必须使用四台机器。
CPU 主线的最后一层调用原文摘录适配器和已有的来源核验 Skill,神经模型调用次数为零。保留模型接口仍然有用:以后换成模型适配器,也要满足同样的输入、输出与停止契约,并重新通过评测。第 47 章单独测量的 Qwen 选择调用,仍只说明那项范围更窄的实验,不能算成本服务新产生的模型样本。
- 应用保存的记录界面:问题与 2026 年日期
- 处理步骤业务:确认调用者,接纳任务
- 模型之外检索:v2 第 1—3 行
- 处理步骤回答适配器与 Skill:核验 750
问题 → 获授权的任务 → 适用资料 → 核验结果 → 界面。进度可以先到,金额必须等核验完成。
- 应用保存的记录收到停止请求:cancelling
- 处理步骤通知进程组停止,并等待退出
- 应用保存的记录只能有一个终止状态:cancelled 或 answered
停止先成功 → 不显示答案,也不写缓存。完成先成功 → 取消返回 409;用户仍可清空页面内容。
图 54.1 把数据传递与停止、返回结果的权限分开。界面提交问题,不能自行指定一个值得信任的租户身份;来源提供制度文字,不能授予查看其他账户的权限。结果是否向用户显示,由宿主决定,不由答案里的一句话决定。
例 54.1:跟踪一次真实的本地请求。 客户端向 POST /api/requests 发送下面的 JSON,并在 Authorization 请求头中携带虚构北区账户的凭据。这个凭据是公开的演示占位值,不是生产密钥。资料库仍保留 v1、v2,排序仍沿用第 44 章的候选顺序,金额单位是元/间夜。
{"question":"Beijing lodging","on_date":"2026-09-14","topic":"travel","locale":"en"}服务先检查调用者和全部四个字段,再分配一个不含业务含义的请求编号,返回 HTTP 202。这表示已经接纳处理,不表示已经回答。客户端可以通过 GET /api/requests/{id} 读取任务。一次成功的 GET 也可能读到“仍在运行”:HTTP 成功说明任务记录已经读到,记录里的状态才说明工作做到了哪一步。这是两个不同的问题。
表 54.1:本机回环请求实际经过的阶段
| 阶段 | 实际发生了什么 | 用户可以据此判断什么 |
|---|---|---|
| 已接纳 | 调用者和输入通过接纳检查,已分配任务编号 | 服务收到了这次请求 |
| 开始运行 | 有额度限制的工作进程开始处理,没有匹配的已存答案 | 工作正在进行 |
| 检索与组织答案 | 日期筛选选中 v2 第 1—3 行,适配器准备好 750 | 已有候选,还不是最终答案 |
| 核验来源 | 经过审查的 Skill 检查原文范围、日期、来源和结论 | 候选满足本项目范围明确的证据契约 |
| 回答完成 | 宿主确定最终结果 | 界面可以同时显示答案与来源 |
返回的来源可以直接阅读,不用再打开另一个工具:
虚构北京差旅住宿制度;金额单位为元/间夜。
自 2026-01-01 起生效,替代 travel-v1。
住宿限额为 750 元。
这是本书原创固定资料中的文字,引用位置为 travel-v2 第 1—3 行。第 1 行给单位,第 2 行说明何时适用,第 3 行给金额。只显示“750”,就丢掉了两个必要条件。若问题日期是 2025-12-31,同一份资料库会选出 v1,返回 600。因此,适用日期应当作为用户看得见、能核对的输入,不能悄悄把每个问题都解释成“今天”。
54.2 流式进度不等于逐段核验过的事实
Section titled “54.2 流式进度不等于逐段核验过的事实”等待完整响应最容易实现,却可能让较长的处理过程看起来像卡住了。流式响应(streaming) 允许连接在工作尚未结束时,陆续传回记录。本例按已经核对的 HTML 标准组织事件流:先写 ID,再写一行 JSON 数据,最后以空行结束。例如,实际发出的一个事件采用下面的结构:
id: 2data: {"id":2,"stage":"running","elapsed_ms":5.057}这里的耗时来自首次保存的本地运行记录,单位是毫秒,从任务接纳时开始计时。客户端读取的是完整事件,不能把任意网络数据块当成事件:一个数据块可能在 JSON 字符串中间结束,另一个也可能包含好几个事件。本例的小型 fetch 读取程序会暂存尚未完整的文字,等到空行边界再处理。它只支持服务实际发出的格式子集,不是通用 EventSource 程序库。
这条摘录答案很短,先显示“6”,再显示“00”,最后纠正为“750”,只会制造本来不必出现的信任问题。因此,服务先传进度,把经过核验的答案作为一个最终结果返回。将来如果接入流式生成的模型,就要明确决定:尚未核验的文字怎样标识,哪些使用方式应当阻止,后续检查失败时怎样处理已经被用户读到的内容。后来补一条警告,不能让用户“没看过”那个数字。
事件连接断开,也不会自动取消任务。重新连接时,可以带上 Last-Event-ID,读取后来保存的事件,无须重新计算答案。最终状态不明时,客户端最多重试读取一次,不会悄悄创建第二个任务。这些都是应用通过接口明确作出的安排,不是“流式”这个词自带的保证。
54.3 等待和重试,都要有共同的上限
Section titled “54.3 等待和重试,都要有共同的上限”本服务最多同时处理两个请求。两个位置都被占用时,第三个请求会收到 503 和重试提示,不会进入一条无限增长的队列。并发(concurrency) 表示工作在时间上重叠,不保证 CPU 同时执行每一条指令,也不保证每秒完成的请求数翻倍。两个名额限制的是活动回答进程,不能让这个教学 HTTP 服务抵御恶意连接洪泛。
例 54.2:重试也计入同一个截止时间。 这里采用教学给定时间,不是服务实测耗时。一个请求从到达到截止共有 1,000 ms。排队花去 120 ms,检索花去 80 ms,一次失败的下游尝试花去 300 ms。重试前再等待 100 ms,剩余时间就是 ms。不能因为尝试编号变了,就再给第二次尝试完整的 1,000 ms。
用 表示单调时钟上的截止时刻, 表示同一时钟的当前读数。单调时钟用于测量经过了多久,不会因为校准日期时间而被拨回。剩余时间为:
、、 必须使用相同的时间单位。如果请求在时钟读数 0 ms 时到达,那么 ,第二次尝试开始时 ,所以 ms。若重试前等待的不是 100 ms,而是 550 ms,就要到 1,050 ms 才能开始,此时 ,不应再发起尝试。取最大值,是为了防止负数超时值在某个程序库里意外获得另一种含义。
实际服务的截止时间为三秒,另外人为加入 50 ms 演示等待,最多尝试两次下游操作,两次之间等 50 ms。这些设置是给定值;观察到的耗时还包括真实的 Python 启动与来源辅助程序执行,不能拿来当模型速度测评。HTTP 和子进程操作也分别限制等待时间。宿主在明确的检查点核对截止时间,必要时停止工作进程;进程清理还会增加有限的调度开销,因此这不是硬实时保证。
超时回答的是“这位观察者是否已经等到上限”,通常不能直接回答“操作到底有没有发生”。本例允许再次读取虚构订单;若要重试已经批准的取消操作,就需要第 49 章的持久意图、回执和恢复检查。本服务没有修改订单的接口,“停止”按钮停止的是回答任务,不是取消订单。
54.4 取消要赢得竞态,不能只藏起结果区域
Section titled “54.4 取消要赢得竞态,不能只藏起结果区域”假设 米拉 在回答进程运行时按下“停止”。只在浏览器里设一个标记,后台仍会继续消耗资源;只在服务端设一个标记,也不会自动终止任意 Python 代码。宿主必须通知活动进程组停止,等待清理完成,并阻止稍后再返回答案。
例 54.3:两种可能先发生的结果。 完成之前,任务尚未进入终止状态。取消请求先把它改成 cancelling,界面说明正在停止。工作进程退出后,宿主才记录 cancelled。完成与取消共用一把锁,让同一时刻只有一个线程能修改受保护的状态。如果取消先成功,最终结果不含答案,也不写入回答缓存;如果完成先成功,取消会返回 409 和已完成状态,界面说明请求已经结束。
实际取消测试会等到一次尝试开始,再发送真实的 HTTP 取消请求,工作进程停止后还要再次检查状态。测试要求不能出现迟到的 answered 事件,也不能留下缓存项。这比检查按钮是否变色更有说服力。进程组实现已在 macOS CPU 环境执行;程序明确要求 POSIX,没有声称支持 Windows。
首次记录暴露了一个更隐蔽的问题。事件顺序是:
attempt → cancel_requested → worker_stopped → dependency_failure → cancelled答案虽然没有返回,但 InterruptedError 被范围更大的 OSError 处理分支捕获,误记成一次下游失败。补上的断言使这条记录无法通过。把取消单独处理后,顺序修复为:
attempt → cancel_requested → worker_stopped → cancelled证据包保留了原始观察和对应回归检查。服务对用户表现正确,仍可能误导运维人员。第 56 章会继续说明,错误的事件分类怎样影响告警。
54.5 结果出来以后,帮助用户作下一步判断
Section titled “54.5 结果出来以后,帮助用户作下一步判断”带引用的答案更容易检查,但引用不能保证原始资料在现实世界中确实具有权威性。因此,界面说明资料是虚构的,同时保留来源日期和单位,不给一条确定的原文摘录附上没有依据的“置信度百分比”。
表 54.2:不同结果,需要不同解释
| 实际请求或操作 | 观察到的结果 | 有用的下一步 |
|---|---|---|
| 查询现行住宿制度 | 750,附 v2 的完整支持范围 | 阅读原文,核对适用日期 |
在差旅资料库中查询 volcano | 没有支持资料,暂不作答 | 细化问题,或询问制度负责人 |
查询 overnight accommodation ceiling | 存在相关制度,却仍暂不作答 | 保留这个词汇覆盖失败,纳入评测 |
| 北区账户读取自己的 A-104 | 虚构订单快照显示“处理中” | 区分快照与商家的实时记录 |
| 北区账户请求无法访问的 B-205 | 拒绝访问,没有修改订单 | 使用有权限的途径,不能换身份绕过 |
| 下游持续不可用 | 返回错误,不编造制度答案 | 等待,或准备说明交给人工处理 |
两次暂不作答,都可以向用户说明“依据不足”,但维护工作不同。前一个问题不在资料范围内,后一个则暴露了已知的词汇缺口。日志和回归样例应当保留这一区别。拒答说得礼貌,不能把漏掉相关资料算成回答质量成功。
本地人工接管(handoff) 功能会准备一段可见说明,列出问题和日期。用户可以自行复制给制度负责人;应用既不发送,也不声称已经有人接手。撤销(undo) 则恢复刚才清空的问题与结果,只反转这次界面变化。若订单已在别处取消,需要另外的业务补偿,而且未必能补偿。
无障碍设计让这些区别更容易被不同用户理解和操作。页面提供看得见、与控件关联的标签;原生控件支持键盘访问;焦点清楚可见;简短的状态区域播报进度,不在每次事件到达时抢走焦点。来源文字可以选择,窄屏也能阅读。这些做法依据 W3C 的相关指南。对标签、焦点和布局的浏览器检查,只能证明相应范围内的行为,不能代替完整的无障碍认证,也不能代替使用辅助技术的用户测试。
54.6 既测试函数,也测试交接处
Section titled “54.6 既测试函数,也测试交接处”工程实践之前要读 HTTP Primer:能够区分客户端与服务端,读懂 JSON 和状态码,避免把凭据写进日志,并解释为什么超时不证明操作没发生。Primer 已经提供可运行的本机回环脚手架。本章新增的阅读台阶是任务(job):由服务端保存、可以在首次 HTTP 响应之后继续存在的记录。事件 ID 表示这个任务内部的记录顺序,不是用户身份。
表 54.3:不同软件层面的证据
| 层面 | 可以观察和检查什么 | 还不能证明什么 |
|---|---|---|
| 纯规则 | 日期与缓存到期边界,剩余时间 | 网络行为 |
| 组件 | RAG 与实际执行的已审查 Skill 子进程 | 界面表达是否让用户理解 |
| 集成 | 真实 HTTP 创建、事件读取、取消、归属和错误 | 生产流量分布下的可靠性 |
| 界面 | 提交、可读来源、键盘焦点、清空与撤销、人工说明和反馈 | 完整的辅助技术使用体验 |
| 概率性评测 | 前文保留的模型样本与留出标准 | 不能由前面任何软件检查代替 |
核心入口是 code/knowledge-assistant/service.py,记录程序和测试位于 code/part-ix/。两个语言界面读取 service-content-v1.json,使用同一份数据与执行逻辑。按项目 README 准备 CPU 环境后,可运行:
python code/knowledge-assistant/service.py --port 0python -m unittest discover -s code/part-ix -p 'test_*.py' -v端口 0 表示请操作系统分配一个可用的本地端口,应打开这个进程实际打印的地址。不需要模型账户、GPU、付费 API 或外部模型服务。程序只绑定 127.0.0.1。选用 Python HTTP 服务是为了教学,不是推荐直接用于生产;配套的带日期在线说明记录了实际测试版本和命令。
至此,KA-9 已有工作量受限的 CPU 演示、真实请求/取消/错误记录、支持无障碍操作的界面,以及跨软件边界的检查。任务和反馈保存在内存中,重启会结束这次会话。订单恢复仍使用第 49 章单独实现的持久 SQLite 机制。把存续时间说清楚,才能避免把“有日志文件”误当成“任务可以持久恢复”。
54.7 本章小结
Section titled “54.7 本章小结”可用的服务把一次计算变成了责任明确的请求。宿主负责身份、接纳、截止时间和终止状态;检索提供适用资料;回答层准备候选;界面帮助用户检查结果并决定下一步。答案尚未核验时,可以先传进度。取消、重试与撤销,各自都需要说清楚会产生什么影响。软件测试能够检查已经覆盖的路径,却不能据此证明模型普遍答得好。
现在已经能检查一次请求的经过。下一步要追问:究竟是哪些版本产生了它,某次修改破坏契约后,又怎样恢复?
54.8 习题
Section titled “54.8 习题”- 例 54.2 仍给 1,000 ms 总额度,把排队改成 200 ms,重试前等待改成 250 ms,检索和首次尝试耗时不变。第二次尝试还可以用多久?若只设一个 800 ms 的单次超时,不加其他限制,是否合适?
- 事件流先显示 600,随后来源核验失败,再显示“请核对”。为什么最后这条提示没有完全修复使用体验?请为制度查询提出一个具体替代方案。
- 南区知道北区的请求编号,并携带自己的有效凭据来读取。哪项检查必须拒绝它?为什么很长的随机编号和成功鉴权都不够?
- 取消接口在工作进程清理完之前就返回了。设计一个可观察的测试,区分“已请求停止”和“已经停止”,并说明之后绝不能出现什么。
- 服务的 HTTP 测试全部通过,却仍对表 54.2 的同义表达暂不作答。现在能否声称它能可靠理解制度问题?还需要什么证据与修复?
54.9 参考解答
Section titled “54.9 参考解答”- 剩余 ms。只设 800 ms 会超过请求总额度。应取常规单次超时与 170 ms 中较小的值,还要另外考虑清理开销。
- 用户可能已经读到甚至使用了 600,警告不能撤回这次接触。对这种短答案,可以只流式显示进度,核验完整且适用的来源后,再一起显示金额与引用。核验失败就明确暂不作答,不显示未经核验的金额。
- 任务归属授权检查必须比较已认证的租户/用户与任务所有者,并拒绝这次读取。随机编号只能降低猜中的机会,不能授予权限;鉴权识别出南区身份,也不会让南区成为北区任务的所有者。
- 启动一个人为延长的请求,等一次尝试开始,再发取消;清理完成前应处于
cancelling。之后要求只有一个取消终止结果,不能再返回答案或写缓存。还应测试相反顺序:已经完成的请求保持完成,取消接口说明来迟了。 - 不能。HTTP 行为只说明这些案例中的传输与状态控制符合约定,没有证明词汇覆盖充分。应保留失败表达,检查切分与检索,在同一评测集上比较修复方案,并继续保留来源和日期检查。前文模型选型失败的证据也仍然有效。
54.10 参考文献
Section titled “54.10 参考文献”- RFC 9110,HTTP Semantics,§§9.2.1–9.2.2、15.3.3。支持区分安全重复、已经接纳和处理完成。
- Python 3.12 HTTP 服务文档。说明本例使用的请求处理与线程机制,以及明确的生产适用限制;实测运行版本另记在配套记录中。
- Python threading:Event 对象。解释线程之间怎样传递信号;进程清理和终止状态规则由本项目另外实现。
- WHATWG HTML:服务端事件,§§9.2.4–9.2.6。规定事件 ID、UTF-8 编码与事件边界。
- WCAG 2.2,SC 2.4.7;理解状态消息;理解标签或说明。支持本章范围明确的无障碍检查,并区分可见说明与程序可识别的名称。
- OWASP LLM09:2025 Misinformation。防范建议支持可见的来源核对、范围说明与人工监督,但没有声称附上引用就一定真实。