带着这套读法去更远的地方
前十五章,你已经把一个真实项目从 Cargo.toml 读到流式(SSE)响应的最后一个字节。最后一章没有新语法,只回答一个问题:接下来去哪。读完你应能回答:
- 三种开发范式里挑一种,你的第一站和第二站分别读什么?
- 把 pi-mono 的分层清单摆到 jackdaw 面前,哪些层对得上、哪两个位置是刻意留空的?
- 当 AI 生成的代码以你的用户权限运行时,你的四条围栏分别挡住什么?
三张阅读地图
后端服务。第一站其实你已经读过了:jackdaw 就是一个 axum 后端。下一站去 axum 官方仓库的 examples 目录,挑两三个 handler,专找书里没出现过的东西——优雅停机、连接池、更复杂的共享状态。之后再进 tokio 官方教程,把第 7 章"await 是让出点"的读法放进完整例子里。
桌面与 CLI 应用。Tauri 应用可以对照 jackdaw 读:前端发请求、Rust 核心应答,结构上就是 agent 与网关的对话。注意类比失配:两边走的是进程内通信而不是 HTTP,前端通常还是 TypeScript。CLI 一侧的 clap,第 10 章读过了。
系统级库。去读 The Embedded Rust Book。那里没有 tokio、没有运行时,常常连堆分配器都没有(no_std);第一次只读前几章就够。目的不是马上写固件,而是检验你的所有权读法离开标准库还成不成:&/&mut 的规则一点没变,消失的是你习惯依赖的基础设施。
TIP
换项目不换读法:先读 Cargo.toml 的依赖段猜职责,再找入口文件。第 8 章的这套顺序,对 axum 的 examples 同样适用。
对照 pi-mono:同一套分层,两种语言
pi 自称 "Pi Agent Harness",定位是可自扩展的 coding agent 工具族,用 TypeScript 写成。它的分层:pi-coding-agent(交互式 CLI)、pi-agent-core(agent 运行时:工具调用与状态管理)、pi-ai(统一多 provider 的 LLM API)、pi-tui、pi-telemetry、pi-durable、chord(应用组合运行时)。与 jackdaw 摆在一起:
| pi 的层 | jackdaw 的对应物 |
|---|---|
| pi-ai(统一多 provider API) | provider.rs 的 trait Provider + openai.rs 实现 |
| pi-agent-core(agent 运行时) | 没有——jackdaw 转发消息,不跑 agent 循环 |
| pi-coding-agent(交互式 CLI)、pi-tui | cli.rs、ask.rs(命令行入口) |
| 没有——pi 不是网关 | gateway.rs + router.rs(协议入口、路由与故障转移) |
这张表要读出两件事。第一,分层的标准不是文件多少,而是"谁依赖谁、失败在哪层处理"。gateway 的模块注释把边界写得很直白:
//! The HTTP gateway: an OpenAI-compatible face in front of the router.
//!
//! Responsibilities here are deliberately narrow — the gateway checks the
//! bearer token, validates the request envelope, then hands everything to
//! [`crate::router`] and translates the outcome into HTTP. All upstream
//! semantics (failover, model rewriting) live in the router, which is why
//! `ask` gets identical behavior for free.
// ...读法:从第二行注释看起,三个动词就是三步——验 token、验请求信封、把剩下的一切交给 crate::router。数据在这层进的是原始字节,出的是 HTTP 响应;失败转移与模型改写都不归它管。也正因为语义全在 router,ask 复用路由时行为完全一致(注释最后一句说的就是这件事)。
第二,表里两个空位本身就是信息:pi 刻意不做网关,jackdaw 刻意不做 agent。还有一条能把两边接起来:pi 支持 custom provider(baseUrl + API key),可以把网关当作一个 provider 使用——换句话说,pi 能把你手上的 jackdaw 配成它的一个上游。第 14 章你练过把 agent 指到 jackdaw,这是同一条路的另一头。
AI 原生安全习惯清单
pi 的哲学是极简核心、透明可审计、可自扩展,其中一条值得抄下来:不内建权限系统,一切以你的用户权限运行,要隔离就自己上容器。AI 原生开发同理——AI 写的代码跑在你的机器、你的密钥、你的文件系统上,没有沙箱替你兜底。你的围栏是四条习惯:
- 规格先行:动手前写下目标、约束、验收。写不出验收,就还没到让 AI 开工的时候(第 3 章)。
- 小步走:一次一个可验证的改动,
cargo check绿了再走下一步(第 8 章)。 - 测试仲裁:先看到失败的测试,再让 AI 改实现,
cargo test全绿才算数(第 15 章)。 - 读 diff:所有权转移、并发、
unsafe、错误被吞,这四类必须自己看懂(第 5 章)。
三个扩展练习
由易到难,都从 jackdaw 的真实现状出发;每题标注会撞上的新概念与建议回看的章节。
一、intent 规则路由。 给路由组加规则:消息里带图片就路由到多模态成员,其余走默认。magpie 的规则路由(tokens ≥ N、含图片、reasoning、agent、intent 内置分类器)比这多得多,从硬规则起步即可。新概念:第一次真正解析请求的 messages(此前除 model 外,body 是整体透传的);规则用枚举表达、以模式匹配穷尽。回看第 6 章、第 11 章、第 13 章。
二、Anthropic 协议翻译。 照 build_app 里两条现有路由的样子,加 POST /v1/messages:收 Anthropic 形状的请求,翻译成 OpenAI 形状交给 router,响应再译回去。magpie 同时说 OpenAI / Anthropic / Gemini 三种协议并互相翻译,这是它的最小切片。新概念:两组类型化结构之间的字段映射(序列化不再靠 derive 白拿)、SSE 事件形状的改写——先做非流式,两边的流式事件格式完全不同。回看第 6、12、14 章。
三、usage 统计。 按模型记录令牌消耗、按组汇总,再加一个 stats 子命令打印。对照 magpie:它的 routing 策略里有一种 usage,你先把统计做出来,以后才有得选。难点是计数要跨请求共享,而答案已经埋在代码里:
/// Owns the config view, the provider instances and the rotate counter.
pub struct Router {
config: Arc<Config>,
providers: HashMap<String, Arc<dyn Provider>>,
/// Monotonic counter behind `rotate`: `fetch_add` is atomic, so
/// concurrent requests always advance to different starts.
rotate_counter: AtomicUsize,
}读法:从最后一个字段 rotate_counter 看起。整个 Router 被 Arc 装着放进 AppState(第 14 章),所有并发请求共用同一个实例,而 complete 只有 &self、拿不到 &mut——所以"跨请求累加"只能走两条路:原子操作(rotate_counter 的 fetch_add 就是一次)或互斥锁。usage 统计是同一模式加一层按模型分桶;第 7 章一句话带过的 Send/Sync,在这里会第一次真正咬人。回看第 7、10、13 章。
小结
三张地图的第一站:axum examples、Tauri、embedded book。pi 与 jackdaw 的对照表里,两个空位和四行对应一样重要;四条围栏——规格、小步、测试、读 diff——成立的前提是 AI 的代码以你的权限运行。三个练习越往后越要直面并发。书到这里结束,读法从现在起归你。
自检
- 要给 jackdaw 加"每分钟最多 N 个请求"的限流,放在 gateway 还是 router?理由是什么?现有代码里位置最相近的先例是哪一个?
- AI 把 usage 计数器声明成
Router的普通usize字段,并在complete里直接+= 1。编译器会拦吗?报在什么环节? - 为什么"pi 不内建权限系统"这一事实,让"读 diff"变得更重要而不是更不重要?
参考答案
- gateway。限流和 Bearer 认证一样,是"动用任何上游之前就该拒绝"的横切关注点,放在边缘层,所有路由路径共享同一个闸门;先例是
chat_completions一进来就执行的authorized检查。router 的候选循环不适合承担这种整请求级别的拒绝。 - 会拦。
complete(&self, ...)拿到的只是不可变借用,+= 1是对&引用背后的字段赋值,借用检查器在编译期就报 E0594 "cannot assign to ..., which is behind a&reference"。这正是rotate_counter选AtomicUsize的原因。 - 没有内建权限系统,意味着进入系统的代码全部以你的用户身份运行,要隔离只能自己动手(比如容器)。防线不在工具里,就只能在进入你仓库的代码里——AI 生成的 diff 正是"以你的权限运行"的代码的主要入口,读 diff 是唯一默认存在的安全边界。