老张是团队里能扛活的骨干。第一次接 Agent 项目,他从零搭了一套——配置、日志、重试、API 路由,吭哧吭哧写了一周。第二次接项目,他翻出上次的代码看了又看,觉得几处不顺手,索性又重写了一套。第三次、第四次……等到第十个项目落地时,他面对十个长得像、却又不完全一样的 config.py,欲哭无泪:线上发现一个重试的 bug,得改十个地方;某个项目的日志漏了 trace_id,排查时两眼一抹黑;新同事入职,光是搞清楚"这本书里到底有几种配置写法"就花了三天。
"早知道,我一开始就该把这副底座搭好。"老张盯着满屏的复制粘贴,喃喃自语。
这一章,我们就来帮老张——也帮未来的你——把这件事一次做对。我们要搭的不是某个具体项目,而是一副能跑十个项目的工程脚手架。它像盖楼用的预制构件:先在工厂里标准化生产好,运到工地一拼,楼就立起来了。后面十个项目,都站在它肩膀上,新增成本几乎为零。
全书主线进度:第 1 章看清了全图——八张核心拼图、十条进化路径。从本章开始,我们把这些认知变成能跑的代码。第一步不是直接写 Agent,而是先把工程底座搭好——这个底座会让后面十个项目的新增成本几乎为零。
本章对应代码:
backend/core/、backend/main.py
2.1 老张的烦恼
2.1.1 业务背景与痛点
回到老张的十个项目。如果每个都独立搭建——配置、日志、Agent 循环、错误处理、API 路由各写一套——会冒出三类毛病:一是重复代码堆积如山;二是十个项目十种风格,改一个 bug 要满世界找;三是读者翻书时也犯嘀咕:“为什么项目一和项目二的配置方式都不一样?到底听谁的?”
根子上的问题只有一个:没有一副共享的骨架。这一章,我们就把这副骨架立起来。
2.1.2 用户故事
把上面的痛点翻译成四条用户故事,每一条都对应一个真实的"谁、想要什么、图什么":
| 编号 | 作为 | 我想要 | 以便 |
|---|---|---|---|
| US-1 | 读者 | 所有项目共享同一套配置与日志体系 | 不用每个项目重新学一遍基础设施 |
| US-2 | 开发者 | 新增一个项目只需继承基类 + 实现 build_agent() | 快速迭代,零样板代码 |
| US-3 | 运营 | 线上服务有健康检查、指标暴露、流控 | 生产环境可运维 |
| US-4 | 读者 | 前端能实时看到 Agent 执行过程 | 学习和验证 |
2.1.3 功能性需求
- FR-1 配置管理:集中式配置,环境变量优先,.env 兜底,启动时验证必需项
- FR-2 结构化日志:开发环境人类可读格式,生产环境 JSON 行格式,自动注入 trace_id
- FR-3 Agent 基类:封装重试、超时、流控、会话管理、指标收集
- FR-4 项目注册表:导入即注册,后端自动发现并暴露 API
- FR-5 统一 API:健康检查、指标暴露、运行(同步)、流式运行(SSE)
- FR-6 错误兜底:工具调用失败、模型超时、并发满、流控拒绝——全部优雅降级,不崩溃
2.1.4 非功能性需求
- 单次 Agent 调用超时 120 秒自动终止
- 模型调用失败最多重试 3 次(指数退避)
- 每会话每分钟最多 60 次请求
- 全局最多 10 个并发 Agent 执行
- 会话空闲 1 小时自动过期
2.2 画个样子:它该长啥样
2.2.1 架构总览
%%{init: {'theme':'base','flowchart':{'useMaxWidth':true,'htmlLabels':true}}}%%
graph TD
subgraph Core["core 公共层(全书复用)"]
Cfg["config.py
配置管理 + 启动验证"]
Log["logging_conf.py
结构化日志 + trace_id"]
AC["agent_core.py
BaseProject + 注册表 + 流控 + 指标"]
end
subgraph Projects["projects 项目层(第3-12章)"]
P1["p01 客服"]
P2["p02 知识库"]
Pn["... p10"]
end
subgraph Serve["服务层"]
API["main.py
FastAPI + 健康检查 + 指标 + SSE"]
end
P1 & P2 & Pn -->|继承 + 注册| AC
API --> AC
Cfg & Log --> AC
classDef coreN fill:#ede7f6,stroke:#5e35b1,stroke-width:2px,color:#311b92
classDef projN fill:#e0f2f1,stroke:#00897b,stroke-width:2px,color:#004d40
classDef serveN fill:#e3f2fd,stroke:#1976d2,stroke-width:2px,color:#0d47a1
class Cfg,Log,AC coreN
class P1,P2,Pn projN
class API serveN
核心设计思想:每个项目只需继承 BaseProject、实现 build_agent(),就自动获得重试、超时、流控、会话管理、指标、日志、API 路由。新增项目零样板。
这里有个类比特别贴切。BaseProject 就像公司发给新员工的入职手册:你不用自己琢磨考勤怎么打、报销怎么贴、邮件怎么发——手册里都写好了,照着走就行。新员工(新项目)要做的事只有一件:填好自己的岗位职责(实现 build_agent()),其余的重试、超时、流控、指标、日志、API 路由,公司流程(基类)全包了。
而 ProjectRegistry 就像公司的花名册:新员工入职那天到 HR 报个到(模块被 import),名字自动登记进系统(注册表),前台(main.py)立刻就能查到、能调度,无需手动改任何路由配置。
2.3 拆开看:怎么造出来
2.3.1 技术选型
| 技术点 | 选型 | 理由 |
|---|---|---|
| 配置管理 | pydantic-settings |
类型安全、环境变量优先、启动时验证 |
| 日志 | 自研结构化日志 | 自动注入 trace_id/project_id/thread_id |
| Agent 编排 | langchain.agents.create_agent |
官方 v1.0 推荐 |
| 模型 | Claude via langchain-anthropic |
长上下文、强推理、经济 |
| 后端 | FastAPI + SSE | 高性能、原生流式支持 |
| 流控 | 自研滑动窗口 | 无外部依赖、per-thread 隔离 |
选型表看着平淡,但每一行背后都有一段"为什么不是它"的故事。挑几个最容易踩坑的,在下文掰开揉碎说。
2.3.2 关键设计决策
决策 1:为什么用 create_agent 而不是 create_react_agent?
这里有个反直觉点,值得讲透。
很多人写 LangChain 习惯了 create_react_agent,书里、博客里到处都是它的身影。可当你翻开官方 v1.0 文档,会看到一行扎眼的标注:langgraph.prebuilt.create_react_agent 已被标记为 deprecated。官方推荐统一使用 langchain.agents.create_agent。
心路历程是这样的:一开始你也许会犹豫——“我用惯了 create_react_agent,网上的例子都是它,换掉会不会出一堆兼容问题?” 但 deprecated 就是 deprecated,今天不换,明天 LangChain 升个版本,你的代码就开始冒警告,后天可能直接报错。与其被动挨打,不如一开始就跟随官方推荐。本书全书统一使用 langchain.agents.create_agent,后面十个项目无一例外。
⚠️ 避坑:在网上搜到老教程用 create_react_agent 时,心里要有个数——那多半是 v1.0 之前的写法,照抄会吃到 deprecation 警告。
决策 2:为什么自己封装重试/超时,而不是依赖 LangChain 内置?
LangChain 确实有内置重试机制,省心。但它的重试策略是全局的——一配全配,没法按项目粒度定制。客服项目希望重试 3 次、知识库项目只想重试 1 次(毕竟每次重试都是要花钱的 Token),内置机制做不到。于是我们自研一层薄薄的封装,换来三个自由度:
- 按项目设置不同的重试次数和退避策略
- 重试过程中记录结构化日志(第几次失败、睡了多久、下次几点再试)
- 最终失败时返回用户友好的兜底文案,而非异常堆栈
💡 顿悟时刻:框架的内置功能不是越省心越好。当你的需求开始"分项目、分场景"时,一层薄薄的自封装,往往比和框架的默认行为较劲要轻松得多。
决策 3:为什么用 SQLite 做短期数据,而不是全内存?
全内存最快,但服务一重启,所有会话记忆灰飞烟灭。SQLite 的好处是"零配置、零运维"——不用装数据库服务,一个文件搞定,适合学习和单机部署。等你的项目真要上生产了,换成 PostgreSQL 只需改一行连接字符串,业务代码一行不用动。
这就叫给未来留一扇门。
2.3.3 模块职责
| 模块 | 文件 | 职责 |
|---|---|---|
| 配置 | core/config.py |
集中管理所有配置项,启动时验证,支持多环境 |
| 日志 | core/logging_conf.py |
结构化日志,自动注入上下文,开发/生产格式切换 |
| Agent 基类 | core/agent_core.py |
BaseProject(重试/超时/流控/指标)、ProjectRegistry |
| 服务 | main.py |
FastAPI 入口,统一路由,健康检查,SSE 流式 |
四个模块、四份职责,泾渭分明。接下来挨个看实现。
2.4 动手写:搭起脚手架
2.4.1 配置管理(config.py)
代码清单 2-1:生产级配置(节选)
1 | class Settings(BaseSettings): |
设计要点:
- 用
pydantic.Field给每个配置项加上ge/le约束,防止非法值进入系统 - 用
@field_validator确保生产环境必须配置 API Key - 用
@lru_cache做单例,全局只加载一次
配置项看着枯燥,但每一个都对应一个真实的"防呆"场景。
temperature 加了 ge=0.0, le=1.0,不是多此一举。曾经有人在生产环境把 temperature 配成了 10,Agent 输出直接放飞自我,回答像喝醉了酒。一行约束,把这种事故堵在启动那一刻。
@field_validator 在生产环境强制要求 API Key——宁可启动时就直接报错,也别等线上第一个请求打过来才发现"哎呀忘配了"。
@lru_cache 做单例——配置只读一次 .env,全局共享同一份。既省 IO,又保证大家看到的是同一套参数,不会出现"这个模块用的是旧配置"的诡异 bug。
⚠️ 避坑:配置的"启动时验证"是性价比最高的防线。许多线上事故,根因都是"某个值该配没配、配错了"。用类型 + 约束 + 校验器三道关,能把八成的问题挡在服务跑起来之前。
2.4.2 结构化日志(logging_conf.py)
代码清单 2-2:结构化日志(节选)
1 | class StructuredFormatter(logging.Formatter): |
设计要点:
- 开发环境用人类可读格式(带时间戳、级别、trace_id),生产环境输出 JSON 行(便于日志聚合到 ELK/Loki)
- 用
contextvars实现请求级别的上下文自动注入——每条日志自动带上 trace_id,无需手动传参
日志最容易写成"两眼一抹黑"。开发时人读,要好看;生产时机器读,要结构化。一个 Formatter 搞不定两件事,于是我们准备了两位"翻译官":HumanFormatter 伺候开发者,StructuredFormatter 伺候 ELK/Loki。
但最妙的是 contextvars 这一步。想象一下:一个请求进来,我们给它发一张"工牌"(trace_id),这张工牌挂在 contextvars 上。从此这个请求处理过程中打的每一条日志,都会自动带上这张工牌——不用你手动 logger.info(f"trace_id={xxx} ...") 满世界传参。排查问题时,拿工牌一搜,整条链路的日志齐刷刷全出来了。
💡 顿悟时刻:contextvars 是异步时代传上下文的正解。它和线程局部变量(thread-local)长得像,但在 async/await 下能正确穿透,不会把 A 请求的 trace_id 串到 B 请求里。在线程池里跑同步代码、在事件循环里跑异步代码,它都能对得上号。
2.4.3 Agent 基类(agent_core.py)
代码清单 2-3:BaseProject 核心方法(节选)
1 | class BaseProject(ABC): |
设计要点:
- 流控:per-thread 滑动窗口,超过限制直接拒绝(不调用模型,省 Token)
- 并发保护:超过
max_concurrent_agent_runs时返回友好提示,防止雪崩 - 重试:指数退避(1.5^n 秒),最多重试 3 次,每次重试前记录 warning 日志
- 兜底:最终失败返回用户友好的中文错误提示,而非框架异常堆栈
- 指标:每次执行自动记录耗时、工具调用次数、错误次数
run() 是整个脚手架的心脏。血在五个腔室里依次流过:流控 → 并发 → 上下文 → 重试 → 指标。我们逐个看。
流控这步最反直觉,值得多聊两句。很多人的第一反应是:“用户请求来了,凭啥拒绝?服务行业不是应该尽量满足吗?” 但想想餐厅叫号:如果不管多少人涌进来都立刻上菜,后厨先崩、食材耗尽,最后所有人都吃不上。流控就是门口那个发号的——超过窗口容量,直接请你稍等,连后厨都不惊动。体现在代码里:rate_limiter.check() 返回 False 时,我们直接返回提示,根本不调用模型。这一步省下的,全是真金白银的 Token。
⚠️ 避坑:限流一定要在"调用模型之前"。如果先调模型再限流,钱已经花了,限流就成了摆设。
并发保护是流控的兄弟:流控管"频率"(每分钟几次),并发管"同时"(此刻几个在跑)。超过 max_concurrent_agent_runs,新请求被温柔劝退,防止雪崩——一个倒、压一片。
重试用指数退避(1.5^n 秒):第一次失败睡 1.5 秒,第二次睡 2.25 秒,第三次睡更久。为什么不是固定间隔?因为瞬时故障往往需要一点恢复时间,越等越该多等一会儿——就像被拒绝后逐渐拉长再约的间隔,既不骚扰对方,又给自己留机会。
兜底这一步,体现的是对用户的体面:模型真挂了,别把一长串 Python 异常堆栈甩到用户脸上,人家看不懂也不关心。返回一句"系统繁忙,请稍后再试",体面地收场。
指标是给运维的眼睛:每次执行自动记下耗时、工具调用次数、错误次数。出了问题,仪表盘上一看便知。
金句:好的基类,是把"该有的体面"都替子类想好了——子类只管业务,杂事基类全兜。
三层架构总则:每个项目都必须这样组织
BaseProject 解决了"跨项目复用"的问题,但单个项目内部的代码该怎么组织?如果任由大家自由发挥,三个月后 service.py 就会膨胀成两千行的怪物,里面塞着 Prompt、工具、数据库访问、API 路由……改一行抖三抖。所以本书所有项目,在 BaseProject 之上再立一条铁律:三层架构,确保代码边界清晰、职责单一、便于测试和复用。
%%{init: {'theme':'base','flowchart':{'useMaxWidth':true,'htmlLabels':true}}}%%
graph TD
subgraph Entry["入口层(Interface Layer)"]
P["project.py
BaseProject 子类
API 入口 + 注册"]
end
subgraph App["应用服务层(Application Layer)"]
S["service.py
Agent 编排 + 业务流程"]
G["graph.py
LangGraph StateGraph
复杂流程编排"]
end
subgraph Domain["领域与基础设施层(Domain & Infrastructure Layer)"]
M["models.py
领域模型 + 状态定义"]
Pr["prompts.py
Prompt 模板 + 系统提示"]
T["tools.py
工具定义 + API 封装"]
R["repositories.py
数据访问 + 向量库"]
Mem["memory.py
短期/长期记忆管理"]
Mcp["mcp.py
MCP 工具服务器配置"]
end
P --> S
P --> G
S --> M & Pr & T & R & Mem & Mcp
G --> M & Pr & T & R & Mem & Mcp
classDef entryN fill:#fff3e0,stroke:#f57c00,stroke-width:2px,color:#e65100
classDef appN fill:#e8f5e9,stroke:#43a047,stroke-width:2px,color:#1b5e20
classDef domainN fill:#e3f2fd,stroke:#1976d2,stroke-width:2px,color:#0d47a1
class P entryN
class S,G appN
class M,Pr,T,R,Mem,Mcp domainN
分层职责明细
| 层级 | 文件 | 核心职责 | 禁止做什么 |
|---|---|---|---|
| 入口层 | project.py |
继承 BaseProject、项目注册、入参校验、会话管理、链路追踪 |
不能写业务逻辑、不能直接调用工具、不能实现 Prompt |
| 应用服务层 | service.py / graph.py |
业务流程编排、Agent 构建、LangGraph 状态流转、工具调用链 | 不能硬编码 Prompt、不能直接操作数据库、不能暴露 API |
| 领域与基础设施层 | models.py |
定义领域模型、Pydantic 数据结构、Graph State | 不能有业务逻辑、不能有外部依赖 |
prompts.py |
存放所有 Prompt 模板、系统提示词、Few-Shot 示例 | 不能写 Python 逻辑、不能调用模型 | |
tools.py |
定义工具、封装外部 API、实现原子业务动作 | 不能编排流程、不能调用 Agent | |
repositories.py |
数据访问层:DB CRUD、向量库检索、缓存操作 | 不能有业务判断、不能调用工具 | |
memory.py |
记忆管理:Checkpointer 配置、长期记忆存取策略 | 不能编排流程、不能直接返回用户 | |
mcp.py |
MCP 服务器配置、跨工具能力编排 | 不能写业务逻辑、不能直接处理用户请求 |
依赖方向约束
必须严格遵守:上层可以依赖下层,下层绝不可以反向依赖上层。
✅ 允许:
project.py导入service.pyservice.py导入models.py、tools.py、prompts.py
❌ 禁止:
tools.py导入service.pymodels.py引用project.pyprompts.py调用service.py中的函数
这条依赖方向,可以用公司组织架构来记:前台(入口层)可以叫部门(应用层)干活,部门可以调用仓库(领域层)里的东西;但仓库里的螺丝钉不能反过来指挥前台,也不能越级去敲 CEO 的门。 一旦下层反向依赖上层,依赖关系就成了死结,改一处牵一片,测试更是无从下手。
💡 顿悟时刻:分层不是为了好看,而是为了"变"。当你要把 SQLite 换成 PostgreSQL,只需改 repositories.py,上层一行不动——这就是分层换来的自由度。
2.4.4 统一 API(main.py)
代码清单 2-4:FastAPI 统一入口(节选)
1 | app = FastAPI(title="AI Agent 实战 · 统一后端", version="1.0.0") |
main.py 是整栋楼的大堂。不管你是来体检的(健康检查)、来看仪表盘的(指标)、来查花名册的(项目列表),还是来办事的(流式运行),都从这扇门进。一个 FastAPI 实例,把所有项目统一暴露成 API,前端只认这一套接口,不必为每个项目单独对接。
2.5 跑一跑:它真的行吗
脚手架是十个项目的地基,地基不牢,地动山摇。所以下面三类测试不是走形式,而是给地基做承重检测:配置能不能挡住非法值、日志能不能产出合格的 JSON、流控能不能在该拒时拒、该放时放。每一条 assert,都是一道验收线。
2.5.1 配置验证测试
1 | def test_config_validation(): |
2.5.2 日志格式测试
1 | def test_structured_log_format(): |
2.5.3 流控测试
1 | def test_rate_limiter(): |
2.6 送上线:让它上班
2.6.1 本地开发
1 | cd backend |
2.6.2 Docker 部署
1 | export ANTHROPIC_API_KEY="sk-ant-..." |
2.6.3 生产环境 Checklist
本地跑通只是起点,上生产才是真考验。下面这份 Checklist,每一条都对应一个"血泪教训":有人把 API Key 写进镜像推到公开仓库,有人忘了限流被一个脚本刷爆账单,有人 CORS 开成 * 被人跨站调用……照着勾,把这些坑一个个填平。
- [ ] 设置
ENVIRONMENT=production - [ ] 配置有效的
ANTHROPIC_API_KEY(通过环境变量,不写进镜像) - [ ] 限制
CORS_ORIGINS为具体域名(非*) - [ ] 接入日志聚合系统(ELK / Loki)
- [ ] 配置 Prometheus 抓取
/api/metrics - [ ] 设置合理的
MAX_CONCURRENT_AGENT_RUNS和RATE_LIMIT_REQUESTS_PER_MINUTE
2.7 回头看:学到了什么
2.7.1 用到的能力回顾
| 能力 | 在本章中的体现 |
|---|---|
| 配置管理 | pydantic-settings 集中配置,启动时验证 |
| 结构化日志 | contextvars 自动注入 trace_id,dev/prod 双格式 |
| 流控 | 滑动窗口 per-thread 限流 |
| 重试 | 指数退避,最多 3 次 |
| 指标 | AgentMetrics 记录运行次数、错误率、耗时 |
| 注册表 | ProjectRegistry 自动发现与路由 |
2.7.2 常见坑
- 忘记配置 API Key 导致生产环境启动失败——第 2.6.3 节生产环境 Checklist 已覆盖
- 重试次数设为 0——建议至少保留 1 次重试,模型 API 偶尔会有瞬时故障
- 流控阈值设太高——默认 60 次/分钟是针对客服场景,高频场景需上调
2.7.3 可扩展方向
- 接入 Redis 做分布式流控(当前是单机内存)
- 接入 LangSmith 做分布式追踪
- 支持多模型供应商(OpenAI/Google),通过配置切换
回头看,老张的十个项目本不必各自为政。一副共享的脚手架,把配置、日志、基类、API、流控、指标一次做对,后面十个项目就站在了同一副骨架上——新增一个项目,继承 BaseProject、实现 build_agent(),收工。
📌 第 2 章(脚手架)完成。 我们搭建了一个能承载十个项目的生产级工程底座——配置、日志、基类、API、流控、指标。下一章,我们用这个底座构建第一个真实项目——智能客服 Agent。
如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。 也欢迎您共享此博客,以便更多人可以参与。 如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。 谢谢 !