Chapter 10

Logfire 观测与生产部署

Agent 上线后最大的痛点是黑盒——用户说"AI 答错了"你怎么查?模型哪一步决策失误?工具调用了几次?花了多少钱?这一章给出 Pydantic AI 在生产环境的完整闭环:Logfire 观测 + FastAPI 模板 + 超时限流 + Docker 部署 + 上线 checklist。

一、为什么 Logfire 和 Pydantic AI 是一对

Logfire 是 Pydantic 团队出品的可观测性平台,底层基于 OpenTelemetry。它和 Pydantic AI 的关系就像 FastAPI 和 Starlette——同家出品,零配置就对齐

用 Logfire 的好处

  • 一行代码接入:logfire.instrument_pydantic_ai()
  • 自动采集每次 run / 工具调用 / 模型 API 调用,带完整参数和返回
  • token、成本、时延自动汇聚到 dashboard
  • 可以在 UI 里直接查 prompt、system、tool args 原文,调 bug 快
  • OTel 标准,迁到其它平台(Datadog/Honeycomb)不用重写

不用 Logfire 的替代

  • Langfuse:开源自托管,功能类似(见《Langfuse 实战》)
  • Datadog APM / New Relic:通用 OTel 接入
  • Helicone / LangSmith:专门 LLM 观测
  • 自撸 print + 日志文件:只推荐纯本地 demo

二、五分钟接入 Logfire

1. 安装 + 登录

pip install "pydantic-ai[logfire]"
logfire auth                # 浏览器打开,OAuth 登录
logfire projects new my-agent
logfire projects use my-agent

执行完后 ~/.logfire/default.toml 会存下 token,本地 python 脚本直接可发数据。

2. 一行代码打点

import logfire
from pydantic_ai import Agent

logfire.configure()                          # 读取默认 token
logfire.instrument_pydantic_ai()             # ★ 关键一行

agent = Agent("openai:gpt-4o-mini", system_prompt="你是助理")
r = agent.run_sync("什么是古法编程?")
print(r.output)

跑完打开 https://logfire.pydantic.dev,你会看到一条完整的 trace 树:

▼ agent run (2.3s) ├─ model request: openai gpt-4o-mini (1.9s) tokens: in=45 out=120 ├─ output validation: ok └─ total cost: $0.00024

3. 把整个应用全量打点

多个库时用 instrument_* 系列,或直接 instrument_all:

logfire.configure(service_name="my-agent", environment="prod")
logfire.instrument_pydantic_ai()
logfire.instrument_fastapi(app)               # FastAPI 端点自动 trace
logfire.instrument_httpx()                    # 外部 HTTP 请求
logfire.instrument_sqlalchemy(engine=engine)  # 数据库
logfire.instrument_redis()                    # Redis

现在一条请求从 HTTP 进来,到 Agent → LLM → 工具里的 SQL → Redis 写回,所有 span 都在一张 trace 上。出问题时把 trace id 甩给后端开发,5 分钟定位

三、Logfire 帮你看到什么

面板看到的东西实战价值
Live Tail最近若干条请求实时流新版本上线后盯着跑一会儿
Traces一次 Agent run 的完整嵌套(agent→model→tool→子 agent)定位"答错"的根因
SQL Explorer用 SQL 查所有 span,跨请求汇总"昨天 tool=get_order 的平均耗时"
Dashboards自定义图表,token/成本/错误率趋势成本报警、性能回归检测
Alerts基于 SQL 条件触发的报警错误率 > 5% / 成本 $/min > 阈值
强烈推荐建的三个 Dashboard:
成本面板:按 provider / model / route 分解 $/hour,发现贵的模型用在了便宜问题上。
延时 P95/P99:按 Agent 名分,优化重点。
工具失败率:某个 tool 失败率高 = 该工具 schema 或实现有问题。

四、手动 span:给业务节点加标签

Pydantic AI 自动打点的是框架层。你的业务代码想查也能查,得手动加 span:

import logfire

async def handle_chat(user_id: str, msg: str):
    with logfire.span("chat_turn", user_id=user_id, msg_preview=msg[:50]):
        history = await load_history(user_id)
        r = await agent.run(msg, message_history=history)
        await save_history(user_id, r.all_messages())
        logfire.info("chat completed", tokens=r.usage().total_tokens)
        return r.output

几个用法要点:

五、成本与 token 追踪

Logfire 自动从每次 model request 里扒 input_tokens / output_tokens,结合 provider 的定价表算美元成本。你能直接在 SQL Explorer 查:

-- 最近 24h 每个 Agent 的总成本
SELECT
  attributes->>'agent_name' AS agent,
  sum(attributes->>'total_cost_usd')::float AS cost
FROM spans
WHERE start_timestamp > now() - '24h'::interval
  AND span_name = 'agent run'
GROUP BY agent
ORDER BY cost DESC;

生产期建议:

六、FastAPI 生产模板

把前面 10 章的东西串起来,给一个能直接 docker run 的最小生产模板:

# app.py
import os
from contextlib import asynccontextmanager
import logfire
import httpx
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from pydantic_ai import Agent, UsageLimits
from pydantic_ai.exceptions import UsageLimitExceeded, ModelHTTPError

# ── Logfire ──
logfire.configure(
    service_name="qa-agent",
    environment=os.getenv("ENV", "dev"),
    send_to_logfire="if-token-present",  # 本地开发无 token 时不发
)
logfire.instrument_pydantic_ai()
logfire.instrument_httpx()

# ── Agent ──
agent = Agent(
    "openai:gpt-4o-mini",
    name="qa-agent",
    system_prompt="你是技术问答助理,回答准确简洁。",
    model_settings={"timeout": 20, "temperature": 0.2},
)

@asynccontextmanager
async def lifespan(app: FastAPI):
    # 启动
    app.state.http = httpx.AsyncClient(timeout=10.0)
    yield
    # 关闭
    await app.state.http.aclose()

app = FastAPI(lifespan=lifespan)
logfire.instrument_fastapi(app)

# ── 路由 ──
class ChatIn(BaseModel):
    user_id: str
    message: str

@app.post("/chat")
async def chat(req: ChatIn):
    with logfire.span("chat", user_id=req.user_id):
        try:
            r = await agent.run(
                req.message,
                usage_limits=UsageLimits(output_tokens_limit=1500, request_limit=6),
            )
            return {"output": r.output, "usage": r.usage().model_dump()}
        except UsageLimitExceeded as e:
            raise HTTPException(429, detail=f"超出预算: {e}")
        except ModelHTTPError as e:
            logfire.error("model upstream failed", status=e.status_code)
            raise HTTPException(502, detail="模型服务不可用,稍后重试")

@app.post("/chat-stream")
async def chat_stream(req: ChatIn):
    async def gen():
        with logfire.span("chat_stream", user_id=req.user_id):
            async with agent.run_stream(req.message) as resp:
                async for chunk in resp.stream_text(delta=True):
                    yield f"data: {chunk}\n\n"
            yield "event: done\ndata: \n\n"
    return StreamingResponse(gen(), media_type="text/event-stream")

@app.get("/healthz")
async def healthz():
    return {"ok": True}

这个模板把前面章节的"基础功"都带上了:

七、超时、重试、限流

多层超时

生产环境三层超时都要配:

# 1. 模型层 - model_settings.timeout
agent = Agent("openai:gpt-4o", model_settings={"timeout": 30})

# 2. HTTP 层 - httpx client(Pydantic AI 底层走 httpx)
from pydantic_ai.providers.openai import OpenAIProvider
import httpx

provider = OpenAIProvider(http_client=httpx.AsyncClient(timeout=30.0))

# 3. 请求级 - asyncio.wait_for 顶层兜底
import asyncio

try:
    r = await asyncio.wait_for(agent.run("..."), timeout=60)
except asyncio.TimeoutError:
    raise HTTPException(504, "处理超时")

429 限流:优雅重试

OpenAI / Anthropic 压力大时会 429。Pydantic AI 不自动重试 429(retries 只重试校验错),需手动:

import asyncio
from pydantic_ai.exceptions import ModelHTTPError

async def run_with_backoff(agent, prompt, max_retries=3):
    for attempt in range(max_retries):
        try:
            return await agent.run(prompt)
        except ModelHTTPError as e:
            if e.status_code == 429 and attempt < max_retries - 1:
                wait = 2 ** attempt + 0.5
                logfire.warn("rate limited, backoff", attempt=attempt, wait=wait)
                await asyncio.sleep(wait)
                continue
            raise
生产推荐走 LiteLLM Proxy 或自建网关。把限流、重试、fallback、多租户配额、观测集中到网关层,Agent 代码只调一个 base_url。比每个 Agent 都写一套稳定性逻辑可靠得多——详见《LiteLLM 实战》。

八、错误分类:告诉调用方"哪一种失败"

Agent 可能以几种完全不同的原因失败。把它们映射到 HTTP 状态码,前端/上游才能做对的处理:

异常原因HTTP前端行为
ValidationError用户输入结构错400提示用户改输入
UsageLimitExceeded超出成本/请求配额429提示额度不足,升级付费
ModelHTTPError(4xx)provider 拒绝(如 key 错)502平台侧错误,联系客服
ModelHTTPError(5xx/429)provider 抖动503自动重试或稍后
UnexpectedModelBehavior输出怎么也修不成目标结构500记录 trace 交开发排查
asyncio.TimeoutError整体超时504告知用户请求太复杂
from pydantic import ValidationError
from pydantic_ai.exceptions import (
    UsageLimitExceeded, ModelHTTPError, UnexpectedModelBehavior,
)

@app.exception_handler(Exception)
async def global_err(request: Request, exc: Exception):
    logfire.exception("unhandled", exc_type=type(exc).__name__)
    match exc:
        case ValidationError():
            return JSONResponse(400, {"error": "input invalid"})
        case UsageLimitExceeded():
            return JSONResponse(429, {"error": "quota exceeded"})
        case ModelHTTPError() as e if 500 <= e.status_code < 600:
            return JSONResponse(503, {"error": "upstream unavailable"})
        case ModelHTTPError():
            return JSONResponse(502, {"error": "provider rejected"})
        case UnexpectedModelBehavior():
            return JSONResponse(500, {"error": "model output invalid"})
        case _:
            return JSONResponse(500, {"error": "internal"})

九、Docker 部署

最小可用的生产 Dockerfile(基于 uv,第 88 门技艺那个):

# syntax=docker/dockerfile:1.7
FROM python:3.12-slim AS base
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 \
    UV_SYSTEM_PYTHON=1 UV_LINK_MODE=copy

# 非 root 用户,降风险
RUN useradd -m -u 10001 app
WORKDIR /app

# 装 uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv

# 依赖层,利用缓存
COPY pyproject.toml uv.lock ./
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --frozen --no-dev

# 代码层
COPY --chown=app:app . .
USER app

EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD python -c "import httpx; httpx.get('http://127.0.0.1:8000/healthz').raise_for_status()"

CMD ["uv", "run", "uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]

配套 docker-compose.yml:

services:
  agent:
    build: .
    ports: ["8000:8000"]
    environment:
      OPENAI_API_KEY: ${OPENAI_API_KEY}
      LOGFIRE_TOKEN: ${LOGFIRE_TOKEN}
      ENV: prod
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 512M
worker 数怎么选?Agent 请求是 IO 密集——等 LLM。一个 worker(uvicorn 进程)内部 async 可以跑几十上百并发。2–4 个 worker 把 CPU 跑满就够,更多 worker 等于白费内存。

十、上线 checklist

把它打印贴墙上,每次上线对一遍:

☐ 代码层
  1. 所有 Agent 都设置了 name=,便于 Logfire 按 agent 维度筛选
  2. 所有 run 都带 usage_limits,防止单请求失控
  3. 有工具的 Agent:model_settings.timeout 配置合理(20-60s)
  4. output_validator 里没有慢操作(每次校验失败会重试,累加开销)
  5. 敏感数据(api key / PII)不会落到 system_prompt 或 tool 返回,被 Logfire 采集
☐ 稳定性层
  1. 全局异常 handler 覆盖了 UsageLimitExceeded / ModelHTTPError / UnexpectedModelBehavior / TimeoutError
  2. 429 有指数退避重试,或走 LiteLLM Proxy 统一处理
  3. 有健康检查接口 /healthz 并接入 k8s livenessProbe
  4. uvicorn workers 数量和内存限额对齐
  5. 本地 load test 过:P99 延时 / 错误率 / 成本预估都合预期
☐ 观测层
  1. logfire.configure(service_name, environment) 配了 env 和服务名
  2. instrument_pydantic_ai() + instrument_fastapi() 都接了
  3. 业务代码关键路径加了 logfire.span,带 user_id/session_id
  4. 建了成本 / 延时 / 工具失败率三张 dashboard
  5. 报警接入:错误率 > 5% / 成本 $/hour > X,触发 PagerDuty 或钉钉
☐ 安全层
  1. API key 走环境变量或 K8s Secret,不进镜像
  2. 用户输入做基本校验(长度 / 敏感词过滤,视业务),防 prompt injection 攻击
  3. Agent 的工具不暴露数据库原表——至少过一层 deps 里的 service 类,有 user_id ownership 校验(第 5 章)
  4. 多租户场景:deps.user_id 必须来自可信来源(JWT),不能从请求体直接读
  5. 容器非 root 用户运行
☐ 上线流程
  1. CI 跑完单测(TestModel/FunctionModel,零 API 调用)
  2. staging 环境跑过 pydantic_evals 金标集,主要指标没下跌
  3. 灰度:先放 5% → 观察 Logfire 半小时 → 再放量
  4. 回滚预案:镜像版本 tag 可一键切回

十一、本章小结

记住:
① Logfire + Pydantic AI 是官方 CP,logfire.instrument_pydantic_ai() 一行接入,trace/cost/token 全自动。
② 业务节点用 logfire.span(name, **attrs) 手动标注,附带 user_id 等属性,支持 dashboard 汇聚。
③ 生产三件套缺一不可:超时(model + http + wait_for)限额(UsageLimits)错误分类(ModelHTTPError / UsageLimitExceeded / UnexpectedModelBehavior)
④ 稳定性建议走 LiteLLM Proxy 统一网关,把重试/限流/fallback 从 Agent 代码里解耦出去。
⑤ 按上线 checklist 跑一遍,别让 Agent 成为生产事故源头。

十二、十章总览

Pydantic AI 从 Agent("gpt-4o") 到生产上线,十章走下来:

类型骨架(Ch1-3)
框架定位、Agent 基础、结构化输出——理解"类型即契约"心法。
能力扩展(Ch4-5)
Tool 接入真实世界、Dependency Injection 像 FastAPI 一样注入依赖。
交互与状态(Ch6-7)
流式 + 消息历史、Graph 状态机编排复杂流程。
协作与质量(Ch8-9)
多 Agent 协作、测试 & Eval 把 Agent 当代码测。
生产落地(Ch10)
Logfire 观测、FastAPI 模板、Docker + checklist 闭环。

这一整套下来,你已经能用 Pydantic AI 写出可 IDE 补全、可 mypy 校验、可 pytest 断言、可 Logfire 观测的 Agent 应用——不再是魔法黑盒,就是一段普通的 Python 代码。

接下来建议:

祝你在 LLM 时代,写出好读、好测、好改的 Agent。古法编程,精神不变——工具再变,能被理解的代码永远是最珍贵的。