收尾 v10:配置、部署与全书回顾

收尾 v10:配置、部署与全书回顾

这是全书的最后一章。从第 02 章用 fetch 裸调一次 LLM 开始,你一路手写到了多智能体委派。现在只剩两件事:第一,把产品最后一块零件装上:配置与部署(这是 21 个技术点里的 TP21);第二,合上书之前,把整个旅程回头看一遍:v01→v10 的架构演进、21 个技术点如何串成一条”从零到产品”的线、以及你现在能怎样反过来读懂主流框架的”魔法”。

本章目标

读完本章并做完配套练习后,你应该能够:

配置与部署:最后一块零件

为什么配置值得单独讲一章

回头看我们的产品,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),
    // ...
  };
}

它只有一个职责:环境变量读取配置,缺省给默认值。这个写法你已经很熟了,但把它当成”生产可用的配置层”来审视,至少有三个问题:

  1. 密钥只能从环境变量来export API_KEY=sk-xxx 在每次打开新终端都要重设,而且 shell 历史会记住它;
  2. 没区分”默认值 / 文件 / 环境变量”三层来源。生产里不同环境(本地、测试、生产)需要不同配置,不能只靠一份环境变量;
  3. 密钥会被打印出来。日志、报错、调试输出都可能把 apiKey 整个打出去——密钥一旦进日志,就是永久泄漏。

本章把配置层升级成三段式.env 文件解析 → 分层优先级合并 → 密钥脱敏。这也是主流产品(Hermes Agent 的配置文档 把敏感信息统一放进 ~/.hermes/.env,其余放 config.yamldotenv 的 README 讲了 .env 的约定与局限)共同的做法,我们手写一份零依赖实现。

密钥管理:三条铁律与 .env 文件

密钥(API key、token、密码)是 Agent 产品里最需要小心的配置。三条铁律:

铁律含义违反的后果
不进代码key 不写在源码里,连”默认值”都不写key 随代码分发,clone 仓库的人都有
不进 gitkey 放在 .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-1234sk-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 前端。单机部署的最小骨架是三件套:

  1. 进程管理:让服务在后台常驻、崩溃自动重启、开机自启。Linux 上是 systemd 的 service 单元(Restart=always);个人服务器上也有 PM2 之类的进程管理器。要点:把服务的生命周期交给进程管理器,不要用 nohup node app.js & 这种”野进程”方式拉起,后者一断连就没了,崩了没人拉起来;
  2. 日志:进程管理器的标准输出重定向到文件(journald 或日志文件),配上按大小/天数的轮转logrotate)。日志要打结构化的(ch20 讲过:JSON 行、带 trace id),并且,按上一节的要求,打日志前先过脱敏
  3. 反向代理:服务默认监听 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

每个版本的”加了什么”,拆成表:

版本章节演进内容新增/组合的技术点
v01ch14CLI 里的完整 Loop:core 与界面分层,第一个能用的产品组合 TP1+5+6+7
v02ch15服务化:Agent Core 与传输层分离,事件协议 + SSE 下发,多会话TP13 事件协议
v03ch16断线恢复:事件序号与重连续传,断开后 Agent 继续跑,中断/抢占TP15 断线恢复
v04ch17Web 前端:事件流 → UI 状态机,流式渲染与工具中间态TP14 前端流式
v05ch18权限与 HITL:工具风险分级、auto/suggest/approve 三模式、审计TP16 权限
v06ch19安全与沙箱:路径逃逸防护、Shell 隔离、prompt injection 缓解TP17 安全
v07ch20可观测与成本:结构化日志/trace、token 成本核算、失败模式看板TP18 可观测
v08ch21MCP 接入:手写 JSON-RPC 2.0 over stdio,远程工具并入注册表TP19 MCP
v09ch22多智能体:subagent 隔离委派、orchestrator-worker、结果回收TP20 多智能体
v10ch23配置与部署:密钥管理、单机部署、生产差距清单(本章)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 的文档里看到某个概念,回来查这张表,就知道它底下是什么

我们手写的(章节)LangChainLangGraphOpenAI 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 / AgentExecutorStateGraph 里的循环图AgentRunnerRunner.run 的 while 循环)
上下文工程(ch09)手动组装/trim_messagestrim_messages、摘要节点内置 memory 与截断
持久层 + 事件存储(ch10/15)无内置(第三方 store)checkpointerMemorySaver/PostgresSaverSessions + Results(跑完的任务结果可恢复)
断线恢复(ch16)基于 checkpointer 的状态续跑RunResult 恢复
事件协议 + SSE(ch15)stream() / astream_eventsstream_events 输出类型
工具风险分级 + HITL(ch18)interrupt()(图内暂停等人工输入)guardrails + handoffs
可观测(ch20)LangSmith / Langfuse 生态同左(astream_events 是 trace 数据源)trace 参数
MCP 接入(ch21)langchain-mcp-adapters同左MCP 工具包内置支持
多智能体委派(ch22)subgraphs(子图 = 独立状态机)handoffsagent 即工具)
配置层(ch23)各框架的 config 参数同左AgentConfig / env 配置

对照表里有两处值得单独说清楚:

  1. LangGraph 的 interrupt() 就是我们的审批门interrupt() 让图在某个节点暂停、把控制权交回外部、等人工输入后继续,也就是 ch18 getApproval 注入做的事情:core 只认签名,执行到敏感工具时暂停,等审批回调。你如果哪天在 LangGraph 文档里看到 interrupt(),可以确认:它就是”人进得来、且不破坏状态机”的那个设计,我们手写过一个简化版。
  2. 多智能体在框架里的两种形态。LangGraph 用 subgraphs(子图共享主图的状态机制,隔离但嵌套);OpenAI Agents SDK 用 handoffs(agent 作为工具被调用、状态整体移交)。我们的 ch22 实现的是委派 + 结果回收:不整体移交状态,只回收终答字符串,它更接近”agents as tools”这一支,也是我们隔离原则的自然延伸。

对照之后你会发现一个规律:框架不创造新机制,它们只是把”我们已经手写过的机制”产品化、参数化、做成约定。你不再需要”信任”框架——你可以审计框架:它说 interrupt,你知道底下是一个等待外部事件的图节点;它说 checkpointer,你知道底下是把图状态快照到 store 并支持恢复。

结语:合上书之前

回顾整个旅程:你在第 01 章学会了用”模型 + 环境(工具)+ 控制循环”描述任何 Agent;第 02-13 章亲手造出了每一个零件;第 14-22 章把它们装成了一台从 CLI 一路演进到多智能体的全栈产品;第 23 章给这台机器装上了配置与部署,也看清了它距离生产还差什么。

接下来的路,三条可选:

  1. 选一个框架,读它的源码。你现在具备的阅读姿势是”找我们自己写过的机制”,LangGraph 的图执行器、Agents SDK 的 Runner 循环,读起来会像老朋友叙旧(LangGraph 文档OpenAI Agents SDK 文档);
  2. 做结业项目:从零实现一个满足规格的 mini 全栈 Agent,你现在知道规格里每一项对应哪个技术点,也知道每项怎么验收(评估、测试、mock model);
  3. 把”生产差距清单”当成路线图:给 v10 加上鉴权、加上备份、加上告警,每一个都是你已经写过零件的工程化问题。

最后,用一句话收束全书:Agent 不是模型的能力,而是程序的结构。结构的每一根梁、每一颗螺丝,你都亲手拧过。以后无论用什么框架、接什么模型,这台机器都是你的。去造点东西吧。

延伸阅读

完成阅读,去做练习 →