Chapter 02

Agent 基础 · system prompt 与 provider 切换

一个 Agent(...) 里到底可以塞多少东西?系统提示静态和动态各用在哪?六家 provider 怎么丝滑切换?同步、异步、流式这三条 API 的边界在哪?

一、先把构造函数拆开看

Agent 是 Pydantic AI 的唯一主角。你和它打交道的方式就两步——构造(Agent(...)),然后调用(run / run_sync / run_stream)。构造函数吃的参数不算少,但每个都有明确职责:

from pydantic_ai import Agent

agent = Agent(
    model="openai:gpt-4o",           # 1. 绑定哪个模型
    output_type=str,                   # 2. 输出类型:str / BaseModel / TypedDict / Union
    system_prompt="你是一位...",       # 3. 静态系统提示
    deps_type=None,                    # 4. 依赖注入类型(第 5 章专讲)
    tools=[],                          # 5. 初始工具列表(也可以用 @agent.tool)
    name="assistant",                 # 6. 可读名字(trace/日志里用)
    model_settings={"temperature": 0}, # 7. 模型参数
    retries=2,                         # 8. 校验失败最多重试几次
    instrument=True,                   # 9. 是否开 Logfire 自动追踪
)
model
"provider:model_name" 的字符串,或者一个 Model 实例。字符串最常用,切 provider 就改这里。
output_type
Agent 最终返回值的类型。默认 str——LLM 原文回答。写成 BaseModel 子类就变成结构化输出(第 3 章详讲)。
system_prompt
静态 system prompt。可以是字符串,也可以是字符串元组(多段拼接)。需要运行时动态生成请用 @agent.system_prompt 装饰器。
model_settings
透传给 provider 的模型参数字典:temperature / max_tokens / top_p / timeout / parallel_tool_calls 等。这是 provider 中立的结构,Pydantic AI 会把它适配成各家 SDK 的实参。
retries
Agent 级的校验失败重试次数——LLM 输出不满足 output_type 或工具参数校验失败时,把错误原文发回去再试几轮。默认 1,生产一般 2-3。
instrument
是否启用 Logfire/OpenTelemetry 自动追踪。设置全局开关请用 Agent.instrument_all(),第 10 章细讲。

二、System Prompt:静态 vs 动态

静态写法:构造时直接传

agent = Agent(
    "openai:gpt-4o",
    system_prompt="你是一位 Python 技术答疑助手。",
)

想要多段拼接?传元组就行,Pydantic AI 会自动用空行拼接成一段:

agent = Agent(
    "openai:gpt-4o",
    system_prompt=(
        "你是一位 Python 技术答疑助手。",
        "回答不超过 3 句话。",
        "不确定的部分必须明说'我不确定',不要编造。",
    ),
)

动态写法:调用时现算

需要把当前时间当前用户当前 A/B 分桶塞进 system prompt?用 @agent.system_prompt 装饰器,每次 run 时它会被调用一次,结果作为 system prompt 的一部分:

from datetime import datetime
from pydantic_ai import Agent, RunContext
from dataclasses import dataclass

@dataclass
class UserDeps:
    name: str
    vip_level: int

agent = Agent(
    "openai:gpt-4o",
    deps_type=UserDeps,
    system_prompt="你是一位电商客服。",   # 静态部分
)

@agent.system_prompt
def add_user_context(ctx: RunContext[UserDeps]) -> str:
    return f"当前用户: {ctx.deps.name}, VIP 等级: {ctx.deps.vip_level}。"

@agent.system_prompt
def add_time() -> str:
    return f"当前北京时间: {datetime.now():%Y-%m-%d %H:%M}。"

result = agent.run_sync("我的 VIP 折扣还剩多少?", deps=UserDeps(name="张三", vip_level=3))

最终 LLM 看到的 system prompt 是三段拼接:

你是一位电商客服。

当前用户: 张三, VIP 等级: 3。

当前北京时间: 2026-05-07 14:22。
静态 vs 动态的抉择 不随调用变化的放静态(角色定义、行为准则);随用户/时间/请求变化的放动态。动态函数是每次 run 都重新执行一次——别在里面写慢操作(查库要记得缓存)。

动态 prompt 函数签名的玩法

三、Provider 切换:一根字符串走天下

Pydantic AI 目前第一方支持的 provider(截至教程编写时):

provider 前缀模型示例安装 extras环境变量
openai:gpt-4o / gpt-4o-mini / o1 / o3-mini[openai]OPENAI_API_KEY
anthropic:claude-opus-4-7 / claude-sonnet-4-6 / claude-haiku-4-5[anthropic]ANTHROPIC_API_KEY
google-gla:gemini-2.5-pro / gemini-2.5-flash[google]GEMINI_API_KEY
google-vertex:同 Gemini,但走 GCP Vertex[vertexai]GCP ADC
groq:llama-3.3-70b / mixtral-8x7b[groq]GROQ_API_KEY
mistral:mistral-large / codestral[mistral]MISTRAL_API_KEY
cohere:command-r-plus[cohere]COHERE_API_KEY
bedrock:anthropic.claude-sonnet-4-v1:0[bedrock]AWS 凭证
ollama:qwen2.5 / llama3.3 / deepseek-r1不需要(走 HTTP)无(本地 11434)

最常见切换方式:换字符串

agent = Agent("openai:gpt-4o")
agent = Agent("anthropic:claude-sonnet-4-6")
agent = Agent("groq:llama-3.3-70b")
agent = Agent("ollama:qwen2.5")

需要自定义 base_url / API key 时:传 Model 实例

from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIModel
from pydantic_ai.providers.openai import OpenAIProvider

# 例:接入 DeepSeek(OpenAI 兼容接口)
model = OpenAIModel(
    "deepseek-chat",
    provider=OpenAIProvider(
        base_url="https://api.deepseek.com/v1",
        api_key="sk-...",
    ),
)
agent = Agent(model)

这条思路可以接入 所有 OpenAI 兼容网关——DeepSeek、Moonshot、智谱、OpenRouter、LiteLLM Proxy、vLLM。这是最大的自由度来源。

Ollama 本地跑

本地跑不花钱,开发调试首选:

# 先起 Ollama
ollama serve
ollama pull qwen2.5
agent = Agent("ollama:qwen2.5")

注意:小模型(7B / 14B)的 function calling 和 structured output 能力良莠不齐——调试逻辑可以用,生产还是得上大模型或服务化 provider。

运行时动态切换模型

每次 run 都可以覆盖构造时的 model:

agent = Agent("openai:gpt-4o")  # 默认

# 这一次临时换 Claude
result = agent.run_sync("...", model="anthropic:claude-sonnet-4-6")

用途:AB 测试、降级路由、只在重要场景用高级模型。

四、三条 API:run_sync · run · run_stream

run_sync:同步版,最简单

result = agent.run_sync("...")
print(result.output)

阻塞当前线程,适合脚本、CLI、Jupyter。内部其实是启动事件循环跑一遍 run()

run:异步版,生产首选

import asyncio

async def main():
    result = await agent.run("...")
    print(result.output)

asyncio.run(main())

在 FastAPI / Starlette / Aiohttp 里直接 await agent.run(...)。这是生产代码的唯一正确选择——LLM 调用是 IO 密集,阻塞的代价太高。

run_stream:流式

async with agent.run_stream("写一首关于 Python GIL 的短诗") as response:
    async for chunk in response.stream_text(delta=True):
        print(chunk, end="", flush=True)
    final = await response.get_output()

run_stream 的常见用途

五、model_settings:温度、超时、并行工具全在这

model_settings 是一个 TypedDict,字段全部可选。Pydantic AI 会把它适配到各家 SDK:

agent = Agent(
    "openai:gpt-4o",
    model_settings={
        "temperature": 0.2,            # 输出确定性
        "max_tokens": 1024,            # 生成上限
        "top_p": 0.95,
        "timeout": 30.0,               # 秒
        "parallel_tool_calls": True,   # 允许一次调多工具
        "presence_penalty": 0.0,
        "frequency_penalty": 0.0,
        "seed": 42,                     # 可重现
    },
)

调用时也能覆盖:

result = await agent.run(
    "...",
    model_settings={"temperature": 0.9},  # 这次调用临时放宽温度
)
不是所有 provider 都支持所有字段 seed/parallel_tool_calls OpenAI 支持,Anthropic 部分不支持;Groq 的 temperature 上限是 2,Ollama 本地模型看具体 backend。不确定就去翻你要用的那家的官方 API 参考。

六、retries:校验失败怎么办

Agent 的 retries 参数和网络重试无关。它控制的是:

此时 Pydantic AI 不直接抛——它把错误原文作为 ToolReturn 发回去,让模型看着错误再改一次。retries=2 意味着最多给模型改两次机会。

from pydantic import BaseModel

class Answer(BaseModel):
    answer: int

agent = Agent("openai:gpt-4o-mini", output_type=Answer, retries=3)
result = agent.run_sync("二乘三等于几?把数字用中文回答")
# 模型第一次可能返回 "六",Pydantic 校验失败(不是 int)
# Pydantic AI 把"int_parsing: Input should be a valid integer"发回去
# 模型第二次大概率会修正为 6

网络重试是另一回事——那是 HTTP 层的事,由底层 SDK(openai/anthropic/httpx)的重试中间件负责。

七、result 里都装了什么

所有 run 返回的都是 AgentRunResult(流式是 StreamedRunResult)。常用属性:

result = agent.run_sync("...")

result.output                  # 最终输出,类型 = output_type
result.all_messages()          # 本次 run 完整消息列表(含 system/user/assistant/tool)
result.new_messages()          # 本次 run 新增的消息(不含 message_history 传入的)
result.usage()                 # Usage 对象:input_tokens / output_tokens / total_tokens
str(result.usage())         # '{"requests":1,"request_tokens":87,"response_tokens":42,...}'

result.all_messages()new_messages() 的返回值可以直接作为下一次 run 的 message_history,实现多轮对话——第 6 章细讲。

八、完整示例:一个简易"模型选择器"

把本章所有要点串起来——按任务难度选模型,带动态 prompt 和超时保护:

import asyncio
from dataclasses import dataclass
from pydantic_ai import Agent, RunContext

@dataclass
class TaskDeps:
    task_type: str      # "code" | "math" | "chat"
    user_tier: str      # "free" | "pro"

agent = Agent(
    "openai:gpt-4o-mini",
    deps_type=TaskDeps,
    system_prompt="你是一位专业的 AI 助手。",
    model_settings={"temperature": 0.3, "timeout": 20.0},
    retries=2,
)

@agent.system_prompt
def tune_by_task(ctx: RunContext[TaskDeps]) -> str:
    if ctx.deps.task_type == "code":
        return "注意:代码要能直接运行,错误要指出。"
    if ctx.deps.task_type == "math":
        return "注意:数学推导要一步一步来。"
    return "自然对话即可。"

async def ask(question: str, deps: TaskDeps):
    # Pro 用户用 gpt-4o,Free 用户用 mini(便宜)
    model = "openai:gpt-4o" if deps.user_tier == "pro" else "openai:gpt-4o-mini"
    result = await agent.run(question, deps=deps, model=model)
    print("回答:", result.output)
    print("用量:", result.usage())

asyncio.run(ask("写个 Python 的快排", TaskDeps(task_type="code", user_tier="pro")))

九、八个常见坑

  1. 忘记设 API Key:不是 Pydantic AI 的报错,是底层 SDK 的 401。先 echo $OPENAI_API_KEY 确认。
  2. Ollama 本地模型不支持结构化输出:很多 7B 模型的 function calling 不靠谱,结构化需求请换到服务化模型。
  3. system_prompt 传了 list(不是 tuple/str):只接受 str | Sequence[str],传 ["a", "b"] 也行,但传字典会报错。
  4. 把慢查询写在动态 system_prompt 里:每次 run 都会执行,很容易变成性能瓶颈。用缓存或 deps 预查。
  5. 混用 run_sync 和 async 场景:已经在 async 函数里还 run_sync() 会报 "cannot be called from a running event loop"。统一用 await agent.run(...)
  6. run_stream 没 async with:底层连接不会释放,并发跑几次就 leak。务必用 async with
  7. retries 跟超时混淆:超时是 model_settings["timeout"],重试是 retries,两者独立。
  8. 切换 provider 后以为行为完全一致:temperature=0 在 OpenAI 和 Claude 的"确定性程度"体感不同;tool calling 的 schema 严格度也有差别。生产前跑一遍 eval(第 9 章)。

十、本章小结

三条底线记住:
Agent(...) 是一次性构造、多次 run 的对象。构造重在绑定 model/output_type/prompt。
② system_prompt 静态放构造,动态放 @agent.system_prompt——后者每次 run 都会执行。
③ 生产代码用 await agent.run(...),脚本/CLI 用 run_sync,需要打字机效果用 run_stream