收尾 v10:配置、部署与全书回顾
收尾 v10:配置、部署与全书回顾
这是全书的最后一章。从第 02 章用 fetch 裸调一次 LLM 开始,你一路手写到了多智能体委派。现在只剩两件事:第一,把产品最后一块零件装上:配置与部署(这是 21 个技术点里的 TP21);第二,合上书之前,把整个旅程回头看一遍:v01→v10 的架构演进、21 个技术点如何串成一条”从零到产品”的线、以及你现在能怎样反过来读懂主流框架的”魔法”。
本章目标
读完本章并做完配套练习后,你应该能够:
- 说出配置加载的演进路线:硬编码 → 环境变量 →
.env文件 → 分层优先级合并; - 遵守**密钥管理三条铁律**:不进代码、不进 git、不落日志(脱敏显示);
- 描述一台 Agent 产品单机部署的最小骨架:进程管理、日志、反向代理;
- 列出从单机 demo 到生产系统还缺什么:多租户、鉴权、弹性、监控告警等差距清单;
- 对照 v01→v10 演进表,说出每个版本给产品加了什么能力、组合了哪些技术点;
- 把 21 个技术点串成一条线,并指出我们手写的每个零件对应 LangChain / LangGraph / OpenAI Agents SDK 里的哪个组件。
配置与部署:最后一块零件
为什么配置值得单独讲一章
回头看我们的产品,lib/config.mjs 从 ch22 起长这样:
export function loadConfig(env = process.env) {
return {
model: env.MODEL ?? 'mock-model',
apiKey: env.API_KEY ?? 'mock-key',
baseUrl: env.BASE_URL ?? 'http://localhost:3218',
temperature: Number(env.TEMPERATURE ?? 0.7),
maxIterations: Number(env.MAX_ITERATIONS ?? 5),
// ...
};
}
它只有一个职责:从 环境变量读取配置,缺省给默认值。这个写法你已经很熟了,但把它当成”生产可用的配置层”来审视,至少有三个问题:
- 密钥只能从环境变量来。
export API_KEY=sk-xxx在每次打开新终端都要重设,而且 shell 历史会记住它; - 没区分”默认值 / 文件 / 环境变量”三层来源。生产里不同环境(本地、测试、生产)需要不同配置,不能只靠一份环境变量;
- 密钥会被打印出来。日志、报错、调试输出都可能把
apiKey整个打出去——密钥一旦进日志,就是永久泄漏。
本章把配置层升级成三段式:.env 文件解析 → 分层优先级合并 → 密钥脱敏。这也是主流产品(Hermes Agent 的配置文档 把敏感信息统一放进 ~/.hermes/.env,其余放 config.yaml;dotenv 的 README 讲了 .env 的约定与局限)共同的做法,我们手写一份零依赖实现。
密钥管理:三条铁律与 .env 文件
密钥(API key、token、密码)是 Agent 产品里最需要小心的配置。三条铁律:
| 铁律 | 含义 | 违反的后果 |
|---|---|---|
| 不进代码 | key 不写在源码里,连”默认值”都不写 | key 随代码分发,clone 仓库的人都有 |
| 不进 git | key 放在 .env,并把 .env 加进 .gitignore | 提交历史里永久留下密钥(即便后来删掉,历史里还在) |
| 不落日志 | 打印、报错、trace 里的密钥一律脱敏 | 日志系统一旦被访问,等于全部凭据被拖走 |
.env 文件解决”环境变量要反复 export”的问题:把 KEY=VALUE 写进项目目录下的 .env,程序启动时读取并合并进运行环境。git 里只提交 .env.example(占位模板),真实 .env 永远在 .gitignore 里。文件本身还要收紧权限:chmod 600 .env,只允许当前用户读写(单机部署的机器上,其他登录用户、备份脚本都不该能读到它)。
.env 的格式约定(我们本章练习要手写解析器):
# comment:注释行
MODEL=mock-model
BASE_URL=http://localhost:3218
API_KEY=sk-live-secret-1234
QUOTED="value with spaces"
ESCAPED="line1\nline2"
规则:逐行解析,# 开头是注释,KEY=VALUE 用第一个 = 切开,值可以带单引号或双引号(剥掉),双引号内支持 \n 等转义。这个格式来自 dotenv 的约定,但我们不装库,自己写 30 行解析器,坚持本书一贯的立场:先手写,再选型。等你想用 dotenv 库时,你已经知道它替你做的是逐行解析,也就知道它解析不了什么(比如多行值、shell 展开、.env 之间互相引用,这些是配置系统的深水区,见下文”生产差距清单”)。
分层优先级:默认值 < .env < 环境变量
一个 配置项可能同时出现在三个地方,取谁的?我们采用业界标准的分层(12-factor app 的配置原则要求”配置与代码严格分离、按环境分组存储”):
| 优先级 | 来源 | 举例 | 用途 |
|---|---|---|---|
| 低 | 内置默认值 | 'mock-model' | 开箱即用,任何环境不配也能跑 |
| 中 | .env 文件 | MODEL=claude-sonnet-4 | 项目级默认:同一仓库所有协作者共享的开发配置 |
| 高 | 进程环境变量 | export MODEL=claude-opus-4 | 环境级覆盖:CI、部署脚本、临时调试 |
合并不是简单的”后者覆盖前者”,还有两个细节:只合并已定义的键(某个层没设的键,留给下一层);数字/布尔字段要显式转换(环境变量全是字符串,Number() 一次,别在调用处散落转换)。合并完成后,得到一个纯数据对象,任何层、任何模块都可以读它,而不必关心它来自哪里。这也是 ch22 的 loadConfig(env) 的延续:接口不变,只是来源从”一个 env 对象”变成”三层来源”。
密钥脱敏:只在进程内持有,不落日志
配置合并完成后,apiKey 在进程里是明文,这是必须的,调用 provider 时需要它。关键约束是:它只能活在内存里,任何输出通道都要脱敏。
脱敏的规则:按键名判定,键名含 api_key / token / secret / password(大小写不敏感)的值视为敏感项;掩码显示,只保留前 4 个字符,其余替换为 ****(如 sk-live-secret-1234 → sk-l****);非敏感项原样显示。打印有效配置、写启动日志、报错信息里带上配置时,一律走脱敏后的视图。
# 脱敏后的"有效配置"(demo 输出摘录:env 覆盖了 model/temperature/host/port)
model claude-opus-4
apiKey sk-l****
baseUrl http://localhost:3218
temperature 0.2
maxIterations 5
host 0.0.0.0
port 8443
注意脱敏不是加密,它防的是”日志里的被动泄漏”,不防”进程内存被读”(那是操作系统与部署层的事)。它是 Agent 产品里最后一道配置防线,和 ch19 的沙箱、ch20 的 trace 一起,构成”密钥不落盘、不落网、不落日志”的三不原则。
单机部署:进程管理、日志、反向代理
我们的产品是一个 Node 后端(HTTP/SSE 服务)+ 可选 Web 前端。单机部署的最小骨架是三件套:
- 进程管理:让服务在后台常驻、崩溃自动重启、开机自启。Linux 上是
systemd的 service 单元(Restart=always);个人服务器上也有 PM2 之类的进程管理器。要点:把服务的生命周期交给进程管理器,不要用nohup node app.js &这种”野进程”方式拉起,后者一断连就没了,崩了没人拉起来; - 日志:进程管理器的标准输出重定向到文件(
journald或日志文件),配上按大小/天数的轮转(logrotate)。日志要打结构化的(ch20 讲过:JSON 行、带 trace id),并且,按上一节的要求,打日志前先过脱敏; - 反向代理:服务默认监听
localhost:3220,对外暴露时在前面架一层反向代理(Nginx / Caddy)。它做三件事:TLS 终结(HTTPS,否则 API key 在网络上裸奔)、域名与路径路由、可选的简单限流。我们的产品把监听地址与端口做成配置(HOST/PORT),正是为了部署时能随时改、能被代理转发,而不用动代码。
这一层不深讲:systemd 单元怎么写、Nginx 配置怎么写,都属于”运维知识”,随时可以查。本章要你带走的是部署的思维模型:服务不裸奔、崩溃能自愈、日志能找得到、流量走加密。
生产差距清单:还缺什么
把我们的 v10 产品放到真实生产环境里,诚实地说,还差这些(这不该让你沮丧——“知道缺什么”本身,就是把 demo 做成产品的开始):
| 差距 | 我们的现状 | 生产需要 | 相关章节 |
|---|---|---|---|
| 多租户与隔离 | 会话按 sessionId 隔离,但没有”用户”概念 | 用户体系、每用户的会话/文件/预算隔离 | ch10 持久层 |
| 鉴权与授权 | HTTP 服务无鉴权,localhost 即信任 | 登录、API token、接口级权限;/approvals 这种敏感端点必须有身份 | ch18 权限 |
| 密钥轮换与保管 | key 明文在进程里、.env 里 | 密钥托管(如系统密钥环)、定期轮换、泄露自动失效 | ch19 安全 |
| 弹性与限流 | 单进程、无重试上限、无并发控制 | 多副本/水平扩容、provider 限流重试(ch02 讲过重试,生产要指数退避)、背压 | ch02 / ch20 |
| 监控与告警 | ch20 有 trace 与成本核算,但无人值守 | 指标采集(延迟/错误率/成本)、阈值告警、失败模式看板(ch20 数据落库了,缺的是”人在旁边盯”) | ch20 可观测 |
| 数据备份与恢复 | SQLite 单文件,无备份策略 | 定期备份、恢复演练(事件存储的追加特性让”回放重建”成为可能) | ch10 持久层 |
| 安全加固 | ch19 的路径防护、shell 隔离都在 | 进程降权运行、依赖漏洞扫描、密钥最小化(每服务一个 key,权限只够自己的) | ch19 安全 |
| 审计合规 | ch18 审批有审计记录 | 审计日志加密留存、合规导出、不可篡改 | ch18 HITL |
参考 LangChain 官方”Going to production”指南(它把生产化拆成:scope 记忆、配置凭据、可观测、评估、权限与部署几个维度)与 build-your-own-agent(10 组件 from-scratch 资源索引,其收尾段同样强调 guardrails、可观测与 HITL 是 demo 与生产的分水岭),你会发现:生产差距清单里没有一个是我们”不会”的,全部是我们”已经写过零件、只是没拼到生产形态”的。本书阶段二的意义正在于此。
v01→v10 架构演进回顾
第 14 章起,我们在同一个产品骨架上逐版演进。把它画成一张图:
flowchart LR
subgraph p1["阶段一 · 拆解零件(ch02-13)"]
A1["HTTP 裸调 ch02"] --> A2["SSE 流式 ch03"] --> A3["Provider 抽象 ch04"]
A3 --> A4["结构化输出 ch05"] --> A5["Tool Calling ch06"] --> A6["最小 Loop ch07"]
A6 --> A7["工具系统 ch08"] --> A8["上下文工程 ch09"] --> A9["持久层 ch10"]
A9 --> A10["记忆检索 ch11"] --> A11["经典范式 ch12"] --> A12["评估 ch13"]
end
subgraph p2["阶段二 · 装配整机(ch14-23)"]
V1["v01 CLI 完整 Loop ch14"] --> V2["v02 服务化·事件协议 ch15"] --> V3["v03 断线恢复 ch16"]
V3 --> V4["v04 Web 流式前端 ch17"] --> V5["v05 权限·HITL ch18"] --> V6["v06 安全·沙箱 ch19"]
V6 --> V7["v07 可观测·成本 ch20"] --> V8["v08 MCP 接入 ch21"] --> V9["v09 多智能体 ch22"]
V9 --> V10["v10 配置·部署 ch23"]
end
A12 --> V1
每个版本的”加了什么”,拆成表:
| 版本 | 章节 | 演进内容 | 新增/组合的技术点 |
|---|---|---|---|
| v01 | ch14 | CLI 里的完整 Loop:core 与界面分层,第一个能用的产品 | 组合 TP1+5+6+7 |
| v02 | ch15 | 服务化:Agent Core 与传输层分离,事件协议 + SSE 下发,多会话 | TP13 事件协议 |
| v03 | ch16 | 断线恢复:事件序号与重连续传,断开后 Agent 继续跑,中断/抢占 | TP15 断线恢复 |
| v04 | ch17 | Web 前端:事件流 → UI 状态机,流式渲染与工具中间态 | TP14 前端流式 |
| v05 | ch18 | 权限与 HITL:工具风险分级、auto/suggest/approve 三模式、审计 | TP16 权限 |
| v06 | ch19 | 安全与沙箱:路径逃逸防护、Shell 隔离、prompt injection 缓解 | TP17 安全 |
| v07 | ch20 | 可观测与成本:结构化日志/trace、token 成本核算、失败模式看板 | TP18 可观测 |
| v08 | ch21 | MCP 接入:手写 JSON-RPC 2.0 over stdio,远程工具并入注册表 | TP19 MCP |
| v09 | ch22 | 多智能体:subagent 隔离委派、orchestrator-worker、结果回收 | TP20 多智能体 |
| v10 | ch23 | 配置与部署:密钥管理、单机部署、生产差距清单(本章) | TP21 配置与部署 |
注意两条主线:横着看,每一版都是在前一版上”加一层能力”,且从不回退已有行为(v03 的断线恢复依赖 v02 的事件协议与序号,v05 的审批门在 v09 的并发委派里原样保留);竖着看,阶段二几乎不重复讲原理,每个新能力引用的都是阶段一已经手写过的技术点(v08 的 MCP 会话状态机,就是 ch21 重用了 ch08 的注册表模式)。这就是”拆解 → 装配”两段式设计的回报:你读到的每一个版本 diff,都只包含一处新东西。
技术点全景回顾:21 个技术点串成一条线
如果只让你带走一张图,请带走这张:把 21 个技术点按”从零到产品”的因果线串起来:
flowchart TD
subgraph core1["调用与决策(ch02-08)"]
T1["TP1 HTTP 裸调<br/>ch02"] --> T2["TP2 SSE 流式<br/>ch03"] --> T3["TP3 Provider 抽象<br/>ch04"]
T3 --> T4["TP4 结构化输出<br/>ch05"] --> T5["TP5 Tool Calling<br/>ch06"]
T5 --> T6["TP6 最小 Agent Loop<br/>ch07"] --> T7["TP7 工具系统工程化<br/>ch08"]
end
subgraph core2["记忆与边界(ch09-13)"]
T8["TP8 上下文工程<br/>ch09"] --> T9["TP9 持久层<br/>ch10"] --> T10["TP10 记忆与检索<br/>ch11"]
T10 --> T11["TP11 经典范式<br/>ch12"] --> T12["TP12 评估<br/>ch13"]
end
subgraph prod["产品化(ch15-20)"]
T13["TP13 事件协议·服务化<br/>ch15"] --> T15["TP15 断线恢复<br/>ch16"] --> T14["TP14 Web 前端流式<br/>ch17"]
T14 --> T16["TP16 权限·HITL<br/>ch18"] --> T17["TP17 安全·沙箱<br/>ch19"] --> T18["TP18 可观测·成本<br/>ch20"]
end
subgraph ext["扩展与收尾(ch21-23)"]
T19["TP19 MCP 接入<br/>ch21"] --> T20["TP20 多智能体<br/>ch22"] --> T21["TP21 配置与部署<br/>ch23"]
end
T7 --> T8
T12 --> T13
T18 --> T19
这条线的逻辑是:先学会和模型说话(TP1-2),再统一与约束它(TP3-5),然后让它在循环里自主行动(TP6-7);行动需要上下文、记忆与持久化(TP8-10),但也要知道自己什么时候该停下来、怎么评价自己(TP11-12);把它变成一台服务(TP13-15),给用户界面与人的监督(TP16-17),让自己可观测、可核算(TP18);最后接入更广的工具生态(TP19-20),以及让这一切可以被配置、被部署(TP21)。
如果按”这本书到底教了什么”来收束,21 个技术点可以归成六个群组:
| 群组 | 技术点 | 一句话 |
|---|---|---|
| 调用基础 | TP1 裸调、TP2 SSE、TP3 Provider | 怎么和 LLM 说话(同步/流式/多厂商) |
| 决策机制 | TP4 结构化输出、TP5 Tool Calling、TP6 Loop、TP7 工具系统 | 模型如何自主决定并执行行动 |
| 记忆与边界 | TP8 上下文、TP9 持久层、TP10 记忆检索、TP11 范式、TP12 评估 | 记住什么、忘掉什么、怎么验证 |
| 产品化 | TP13 服务化、TP14 前端、TP15 断线恢复、TP16 HITL | 从函数变成一台可用、可交互的服务 |
| 工程化 | TP17 安全、TP18 可观测 | 敢跑出去的前提:防得住、看得清 |
| 扩展与收尾 | TP19 MCP、TP20 多智能体、TP21 配置部署 | 接入生态、拆分协作、落地生产 |
主流框架对照:你手写的零件,在框架里叫什么
第 01 章我们说过:“框架里那些’智能’的编排逻辑,正是本书每一章手写过的零件”。现在你手写完了,是时候做翻译了,把我们的每个零件,对照到主流框架的组件上。这张表是给未来的你查的:任何时候你在 LangChain / LangGraph / OpenAI Agents SDK 的文档里看到某个概念,回来查这张表,就知道它底下是什么。
| 我们手写的(章节) | LangChain | LangGraph | OpenAI Agents SDK |
|---|---|---|---|
| Provider 抽象 + HTTP 调用(ch04) | ChatModels 模型抽象层 | 同左 | model 注入参数 |
| 结构化输出(ch05) | with_structured_output | 同左 | output_type 参数 |
| 工具 schema + 注册表(ch06/08) | @tool 装饰器 | tools 节点 | function_tool 装饰器 |
| 最小 Agent Loop(ch07) | create_react_agent / AgentExecutor | StateGraph 里的循环图 | AgentRunner(Runner.run 的 while 循环) |
| 上下文工程(ch09) | 手动组装/trim_messages | trim_messages、摘要节点 | 内置 memory 与截断 |
| 持久层 + 事件存储(ch10/15) | 无内置(第三方 store) | checkpointer(MemorySaver/PostgresSaver) | Sessions + Results(跑完的任务结果可恢复) |
| 断线恢复(ch16) | — | 基于 checkpointer 的状态续跑 | RunResult 恢复 |
| 事件协议 + SSE(ch15) | — | stream() / astream_events | stream_events 输出类型 |
| 工具风险分级 + HITL(ch18) | — | interrupt()(图内暂停等人工输入) | guardrails + handoffs |
| 可观测(ch20) | LangSmith / Langfuse 生态 | 同左(astream_events 是 trace 数据源) | trace 参数 |
| MCP 接入(ch21) | langchain-mcp-adapters | 同左 | MCP 工具包内置支持 |
| 多智能体委派(ch22) | — | subgraphs(子图 = 独立状态机) | handoffs(agent 即工具) |
| 配置层(ch23) | 各框架的 config 参数 | 同左 | AgentConfig / env 配置 |
对照表里有两处值得单独说清楚:
- LangGraph 的
interrupt()就是我们的审批门。interrupt()让图在某个节点暂停、把控制权交回外部、等人工输入后继续,也就是 ch18getApproval注入做的事情:core 只认签名,执行到敏感工具时暂停,等审批回调。你如果哪天在 LangGraph 文档里看到interrupt(),可以确认:它就是”人进得来、且不破坏状态机”的那个设计,我们手写过一个简化版。 - 多智能体在框架里的两种形态。LangGraph 用 subgraphs(子图共享主图的状态机制,隔离但嵌套);OpenAI Agents SDK 用 handoffs(agent 作为工具被调用、状态整体移交)。我们的 ch22 实现的是委派 + 结果回收:不整体移交状态,只回收终答字符串,它更接近”agents as tools”这一支,也是我们隔离原则的自然延伸。
对照之后你会发现一个规律:框架不创造新机制,它们只是把”我们已经手写过的机制”产品化、参数化、做成约定。你不再需要”信任”框架——你可以审计框架:它说 interrupt,你知道底下是一个等待外部事件的图节点;它说 checkpointer,你知道底下是把图状态快照到 store 并支持恢复。
结语:合上书之前
回顾整个旅程:你在第 01 章学会了用”模型 + 环境(工具)+ 控制循环”描述任何 Agent;第 02-13 章亲手造出了每一个零件;第 14-22 章把它们装成了一台从 CLI 一路演进到多智能体的全栈产品;第 23 章给这台机器装上了配置与部署,也看清了它距离生产还差什么。
接下来的路,三条可选:
- 选一个框架,读它的源码。你现在具备的阅读姿势是”找我们自己写过的机制”,LangGraph 的图执行器、Agents SDK 的 Runner 循环,读起来会像老朋友叙旧(LangGraph 文档、OpenAI Agents SDK 文档);
- 做结业项目:从零实现一个满足规格的 mini 全栈 Agent,你现在知道规格里每一项对应哪个技术点,也知道每项怎么验收(评估、测试、mock model);
- 把”生产差距清单”当成路线图:给 v10 加上鉴权、加上备份、加上告警,每一个都是你已经写过零件的工程化问题。
最后,用一句话收束全书:Agent 不是模型的能力,而是程序的结构。结构的每一根梁、每一颗螺丝,你都亲手拧过。以后无论用什么框架、接什么模型,这台机器都是你的。去造点东西吧。
延伸阅读
- The 12-Factor App · Config:配置与代码分离、按环境存储的权威原则,本章配置分层直接对应。
- dotenv:
.env文件解析的事实标准库;读过本章练习后,你能看懂它替你做的一切。 - Hermes Agent · 配置文档:真实 Agent 产品的配置体系(
.env放敏感信息、config.yaml 放非敏感项、脱敏日志),本章机制参考。 - LangChain · Going to production:LangChain 官方的 Agent 生产化清单,与本章”生产差距清单”对照阅读。
- TheSeydiCharyyev/build-your-own-agent:Agent 技术栈 10 组件的 from-scratch 资源索引,收尾段与本章一样强调生产差距。
- Anthropic · Building effective agents:第 01 章推荐过的权威文章,现在重读它,你会发现”workflow vs agent”之外,你还读得懂它的每个模式图(对应本书哪一章)。