一、PydanticAI 介绍
在本专栏前两篇文章中,我们围绕 分布式多智能体 这一核心命题展开了完整的工程实践。
第一篇基于 AgentScope + A2A + Nacos,解决了异构 Agent 之间的通信与协作难题。A2A(Agent-to-Agent)协议统一了不同框架下智能体的消息格式与交互规范,Nacos 担任注册中心角色,实现了 Agent 服务的注册、发现与负载均衡,最终达到 Python 端多个 Agent 实例组成集群、客户端无感切换。文章地址如下:
第二篇则将视角延伸到了 Java 生态,基于 Spring AI Alibaba 快速搭建 A2A 服务端与客户端,实现了 Java 智能体注册至 Nacos 并被 Python 客户端远程调用,打通了 Java 与 Python 两套技术栈之间的协作壁垒,文章地址如下:
然而,前两篇文章的重心在于 智能体之间如何协同 ,对于 单个 Python 智能体本身的构建 这个问题上,如何以一种更贴近 Python 开发习惯、更灵活、更生产就绪的方式来实现,还有很多的探索空间。
回到单个 Agent 的构建本身,工程上真正棘手的问题往往不是如何调用模型,而是模型的输出是不是可控,参数类型是否安全、运行时的上下文依赖能否统一管理等等,而 PydanticAI 的出现正是从这个角度切入。

PydanticAI 是由 Pydantic 团队(FastAPI、Pydantic 背后的团队)推出的新一代 Agent 开发框架,它的核心目标是:以类型安全、结构化、可观测的方式构建生产级 AI Agent。
与其他框架不同,PydanticAI 充分利用了 Pydantic 在数据验证上的优势,将模型输入输出全程纳入类型体系管理,同时提供了完备的流式输出、工具调用、依赖注入、Capability 扩展等机制,让 Agent 开发更工程化、更易于测试和维护。
核心特性:
- 类型安全 :基于
Pydantic的数据验证,输入输出全程类型检查。 - 结构化输出 :原生支持将模型输出解析为
Pydantic模型或Union类型。 - 多模型支持 :
OpenAI、Anthropic、Gemini、Ollama等主流模型开箱即用。 - 依赖注入 :内置优雅的依赖注入机制,通过
deps_type与RunContext实现运行时依赖传递。 - 流式输出 :提供
run_stream、iter、stream_events等多种流式接口。 - Capability 扩展 :通过
AbstractCapability实现 Agent 全生命周期Hooks拦截与扩展。 - 可观测 :支持
Logfire/OpenTelemetry,工具调用、思考过程全链路可见。
二、快速上手
2.1 安装
bash
pip install pydantic-ai
## 或者
uv add pydantic-ai
根据使用的模型,安装对应扩展:
bash
pip install "pydantic-ai-slim[openai]" # OpenAI / DeepSeek / 兼容接口
pip install "pydantic-ai-slim[anthropic]" # Claude
pip install "pydantic-ai-slim[vertexai]" # Google Gemini
2.2 创建 Agent
PydanticAI 的核心入口是 Agent 类:
python
from pydantic_ai import Agent
agent = Agent(
'openai:deepseek-v4-pro',
instructions='你叫小毕超,是一个超级人工智能助手。',
)
if __name__ == '__main__':
## 同步调用
result = agent.run_sync('你是谁?能做什么?')
print(result.output)
模型字符串格式 为 {provider}:{model_name}, 例如:
openai:gpt-4o:调用OpenAI的gpt-4oopenai:deepseek-v4-pro:通过OpenAI兼容接口调用DeepSeekanthropic:claude-sonnet-4-5:调用Anthropic的Claude
OpenAI 格式下如果需要使用 Chat Completions API格式,可以使用前缀为:openai-chat,如 openai-chat:deepseek-v4-pro。
模型连接信息,PydanticAI 会自动读取环境变量中的值,因此可以通过环境变量配置模型连接信息,例如使用 ModelScope 上的在线免费API:
bash
export OPENAI_BASE_URL=https://api-inference.modelscope.cn/v1
export OPENAI_API_KEY=sk-xxx ## 你的 ModelScope 上的 api key
执行体运行效果:

2.3 注册工具
PydanticAI 提供了 @agent.tool 装饰器可以快速为 Agent 注册工具,同时为了保证上下文的联通性,工具函数的第一个参数必须是 RunContext,在工具内可以通过 RunContext 获取到更多依赖以及上下文信息。
例如:
python
from pydantic_ai import Agent, RunContext
agent = Agent(
'openai:deepseek-v4-pro',
instructions='你叫小毕超,是一个天气助手。',
)
@agent.tool
async def weather_forecast(ctx: RunContext, location: str) -> str:
"""获取指定地区的天气预报"""
return f'{location} 地区的天气为 晴,25°C,微风。'
result = agent.run_sync('南京地区的天气如何?')
print(result.output)
运行效果:

当然 RunContext 也不是必须的,如果不需要 RunContext ,则可以使用 @agent.tool_plain 定义工具:
python
from pydantic_ai import Agent
agent = Agent(
'openai:deepseek-v4-pro',
instructions='你叫小毕超,是一个天气助手。',
)
@agent.tool_plain
async def weather_forecast(location: str) -> str:
"""获取指定地区的天气预报"""
return f'{location} 地区的天气为 晴,25°C,微风。'
result = agent.run_sync('南京地区的天气如何?')
print(result.output)

三、运行模式
3.1 同步与异步
PydanticAI 同时支持同步和异步运行:
python
## 同步调用
result = agent.run_sync('你好')
print(result.output)
## 异步调用
import asyncio
async def main():
result = await agent.run('你好')
print(result.output)
asyncio.run(main())
3.2 流式输出
PydanticAI 提供了丰富的流式事件类型,通过 agent.iter 可以拿到完整的事件体系。
python
from pydantic_ai import Agent, RunContext, PartStartEvent, ThinkingPart, PartDeltaEvent, TextPartDelta, \
ThinkingPartDelta, ToolCallPartDelta, FinalResultEvent, FunctionToolCallEvent, FunctionToolResultEvent, TextPart
agent = Agent(
'openai:deepseek-v4-pro',
instructions='你叫小毕超,是一个天气助手。',
)
@agent.tool_plain
async def weather_forecast(location: str) -> str:
"""获取指定地区的天气预报"""
return f'{location} 地区的天气为 晴,25°C,微风。'
async def stream_shell_output(node, run, output_tools_result=False):
if Agent.is_model_request_node(node):
thinking, answer = "", ""
async with node.stream(run.ctx) as request_stream:
final_result_found = False
async for event in request_stream:
if isinstance(event, PartStartEvent):
print("\n------------------PartStartEvent---------------------")
print(f'Starting part {event.index}: {event.part!r}')
thinking, answer = "", ""
if isinstance(event.part, ThinkingPart):
thinking_part = event.part.content
if not thinking and thinking_part:
print("\n------------------Thinking---------------------")
thinking += thinking_part
print(thinking_part, end="", flush=True)
if isinstance(event.part, TextPart):
answer_part = event.part.content
if not answer and answer_part:
print("\n------------------Answer---------------------")
print("LLM回答: ", end="", flush=True)
answer += answer_part
print(answer, end="", flush=True)
elif isinstance(event, PartDeltaEvent):
if isinstance(event.delta, ThinkingPartDelta):
delta = event.delta.content_delta
if not thinking:
print("\n------------------Thinking---------------------")
if delta:
print(delta, end="", flush=True)
thinking += delta
elif isinstance(event.delta, TextPartDelta):
delta = event.delta.content_delta
if not answer:
print("\n------------------Answer---------------------")
print("LLM回答: ", end="", flush=True)
print(delta, end="", flush=True)
answer += delta
elif Agent.is_call_tools_node(node):
async with node.stream(run.ctx) as handle_stream:
async for event in handle_stream:
if isinstance(event, FunctionToolCallEvent):
print("\n------------------Tool Call---------------------")
print(
f'Calls tool={event.part.tool_name!r} with args={event.part.args} (tool_call_id={event.part.tool_call_id!r})'
)
elif isinstance(event, FunctionToolResultEvent):
if output_tools_result:
print(
f'Tool call {event.tool_call_id!r} returned => {event.part.content}'
)
elif Agent.is_end_node(node):
print("\n")
async def main():
async with agent.iter('南京地区的天气如何?') as run:
async for node in run:
await stream_shell_output(node, run)
if __name__ == '__main__':
import asyncio
asyncio.run(main())
执行输出:

详细执行过程如下所示:
shell
------------------PartStartEvent---------------------
Starting part 0: ThinkingPart(content='', id='rs_chatcmpl-383769a7-6d4b-90e6-9d58-7daa9cca9036', provider_name='openai')
------------------Thinking---------------------
用户想知道南京地区的天气情况。我需要调用天气工具来获取南京的天气预报。
让我调用 weather_forecast 函数,参数 location 为 "南京"。
------------------PartStartEvent---------------------
Starting part 1: TextPart(content='好的,', id='msg_chatcmpl-383769a7-6d4b-90e6-9d58-7daa9cca9036', provider_name='openai')
------------------Answer---------------------
LLM回答: 好的,让我为您查询南京的天气情况
------------------PartStartEvent---------------------
Starting part 2: ToolCallPart(tool_name='weather_forecast', args='', tool_call_id='call_151bb8a7e70e45da86250256', id='fc_call_151bb8a7e70e45da86250256', provider_name='openai')
------------------Tool Call---------------------
Calls tool='weather_forecast' with args={"location": "南京"} (tool_call_id='call_151bb8a7e70e45da86250256')
------------------PartStartEvent---------------------
Starting part 0: ThinkingPart(content='', id='rs_chatcmpl-5286b7b5-cf1c-9a10-963f-7702fc7da039', provider_name='openai')
------------------Thinking---------------------
天气结果出来了:南京地区晴天,25°C,微风。这是一个不错的天气,让我把这个结果整理一下告诉用户。
------------------PartStartEvent---------------------
Starting part 1: TextPart(content='为您', id='msg_chatcmpl-5286b7b5-cf1c-9a10-963f-7702fc7da039', provider_name='openai')
------------------Answer---------------------
LLM回答: 为您查询到**南京**的天气情况如下:
- ☀️ **天气**:晴
- 🌡️ **温度**:25°C
- 🌬️ **风力**:微风
今天南京天气不错,阳光明媚,温度舒适,非常适合外出活动!如果您需要了解其他地区的天气,随时告诉我哦~
另外流式输出,还可以通过 Capability 中的 wrap_run_event_stream 实现,可以更好的做到和主流程解耦,后续内容会演示实现过程。
四、依赖注入
4.1 deps_type
当 Agent 工具需要访问外部服务(数据库、HTTP 客户端、用户上下文等),可以通过 deps_type 注入运行时依赖:
python
from dataclasses import dataclass
from datetime import date, datetime
from pydantic_ai import Agent, RunContext
@dataclass
class WeatherService:
async def get_forecast(self, location: str, forecast_date: date) -> str:
return f'{location} 地区 {forecast_date} 日 24°C 晴。'
async def get_historic_weather(self, location: str, forecast_date: date) -> str:
return f'{location} 地区 {forecast_date} 日 18°C 多云。'
agent = Agent[WeatherService, str](
'openai:deepseek-v4-pro',
deps_type=WeatherService,
output_type=str
)
@agent.system_prompt()
async def build_system(ctx: RunContext[WeatherService]):
now_data = str(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
return f"""
你叫小毕超,是一个天气助手。
当前时间:{now_data} 。
"""
@agent.tool
async def weather_forecast(
ctx: RunContext[WeatherService],
location: str,
forecast_date: date,
) -> str:
if forecast_date >= date.today():
return await ctx.deps.get_forecast(location, forecast_date)
else:
return await ctx.deps.get_historic_weather(location, forecast_date)
async def main():
result = await agent.run('南京今天的天气如何?', deps=WeatherService())
print(result.output)
print("--"*50)
result = await agent.run('南京昨天的天气如何?', deps=WeatherService())
print(result.output)
if __name__ == '__main__':
import asyncio
asyncio.run(main())
通过 RunContext[WeatherService] 泛型声明,工具内部可以通过 ctx.deps 访问注入的依赖对象。
运行输出:

4.2 动态 System Prompt
在上面的示例中已经使用了动态 System Prompt,通过@agent.system_prompt 注解,可以将依赖 ctx.deps中的信息放置到 Prompt 中:
python
from datetime import datetime
from dataclasses import dataclass
from pydantic_ai import Agent, RunContext
@dataclass
class Dep:
user_id: str
session_id: str
agent = Agent(
'openai:deepseek-v4-pro',
deps_type=Dep,
output_type=str
)
@agent.system_prompt
async def get_system_prompt(ctx: RunContext[Dep]) -> str:
now_data = str(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
return f"""
你叫小毕超,是一个天气助手。
当前时间:{now_data} 。
用户ID: {ctx.deps.user_id}
"""
五、结构化输出
5.1 Pydantic 模型输出
output_type 参数支持传入 Pydantic 模型,Agent 会自动将模型响应解析为结构化对象:
python
from dataclasses import dataclass
from pydantic_ai import Agent
@dataclass
class WeatherResult():
location: str
temperature: float
condition: str
agent = Agent(
'openai:deepseek-v4-pro',
output_type=WeatherResult,
instructions='你叫小毕超,是一个天气助手。',
)
@agent.tool_plain
async def weather_forecast(location: str) -> str:
"""获取指定地区的天气预报"""
return f'{location} 地区的天气为 晴,25°C,微风。'
async def main():
result = await agent.run('南京今天的天气如何?')
report = result.output
print(f'地点: {report.location}')
print(f'温度: {report.temperature}°C')
print(f'天气: {report.condition}')
if __name__ == '__main__':
import asyncio
asyncio.run(main())
运行结果:

5.2 Union 类型输出(分流决策)
Union 类型输出适用于需要根据用户意图进行分流决策的场景,例如快速回答和深度分析的双轨路由。
注意:每个类型最好有稳定的判别字段(如 decision),帮助模型稳定产出正确格式。
python
from dataclasses import dataclass
from pydantic_ai import Agent
from pydantic import Field
from typing import Literal
@dataclass
class DirectAnswer():
decision: Literal['direct'] = 'direct'
answer: str = ""
confidence: float = Field(ge=0.0, le=1.0)
@dataclass
class NeedAnalysis():
decision: Literal['analysis'] = 'analysis'
reason: str = ""
RouterDecision = DirectAnswer | NeedAnalysis
agent = Agent(
'openai:deepseek-v4-pro',
output_type=RouterDecision,
instructions='你叫小毕超,是一个天气助手。',
)
@agent.tool_plain
async def weather_forecast(location: str) -> str:
"""获取指定地区的天气预报"""
return f'{location} 地区的天气为 晴,25°C,微风。'
async def main():
questions = [
"南京今天的天气如何?",
"分析一下小毕超博主的风格"
]
for q in questions:
result = await agent.run(q)
decision = result.output
if isinstance(decision, DirectAnswer):
print(f'直接回答: {decision.answer}')
else:
print(f'需要分析,原因: {decision.reason}')
if __name__ == '__main__':
import asyncio
asyncio.run(main())
运行效果:

六、Capability 扩展机制
Capability 是 PydanticAI 最强大的扩展机制,允许开发者在 Agent 的完整生命周期中注入自定义逻辑,实现日志记录、监控、缓存、熔断、工具拦截等能力。
6.1 Capability 生命周期 Hooks
通过继承 AbstractCapability 并实现对应的 Hooks 方法,可以在 Agent 运行的各个阶段插入自定义逻辑:
python
from dataclasses import dataclass
from pydantic_ai import Agent, RunContext, AgentRunResult, ModelRequestContext, ModelResponse, ToolCallPart, \
ToolDefinition
from pydantic_ai.capabilities import (
AbstractCapability, AgentNode, NodeResult,
WrapNodeRunHandler, WrapModelRequestHandler,
RawToolArgs, ValidatedToolArgs, WrapToolValidateHandler,
WrapToolExecuteHandler, WrapRunHandler
)
from typing import Any, AsyncIterable
@dataclass
class LoggerCapability(AbstractCapability[Any]):
"""日志记录 Capability,观察 Agent 运行的完整生命周期"""
## Run 级别 Hooks
async def before_run(self, ctx: RunContext) -> None:
print(f'Agent 开始运行')
async def after_run(self, ctx: RunContext, *, result: AgentRunResult) -> AgentRunResult:
print(f'Agent 运行完成,输出: {result.output}')
return result
async def wrap_run(self, ctx: RunContext, *, handler: WrapRunHandler) -> AgentRunResult:
print('包装整个 Run 执行')
return await handler()
## 节点级别 Hooks
async def before_node_run(self, ctx: RunContext[Any], *, node: AgentNode[Any]) -> AgentNode[Any]:
print(f'节点开始: {type(node).__name__}')
return node
async def after_node_run(self, ctx: RunContext[Any], *, node: AgentNode[Any], result: NodeResult[Any]) -> NodeResult[Any]:
print(f'节点结束: {type(node).__name__}')
return result
## 模型请求级别 Hooks
async def before_model_request(self, ctx: RunContext[Any], request_context: ModelRequestContext) -> ModelRequestContext:
print('模型请求前')
return request_context
async def after_model_request(self, ctx: RunContext, *, request_context: ModelRequestContext, response: ModelResponse) -> ModelResponse:
print(f'模型响应后: {response.parts}')
return response
async def wrap_model_request(self, ctx: RunContext, *, request_context: ModelRequestContext,
handler: WrapModelRequestHandler) -> ModelResponse:
print('包装模型请求')
return await handler(request_context)
## 工具执行级别 Hooks
async def before_tool_execute(self, ctx: RunContext, *, call: ToolCallPart, tool_def: ToolDefinition,
args: ValidatedToolArgs) -> ValidatedToolArgs:
print(f'工具执行前: {call.tool_name}, 参数: {args}')
return args
async def after_tool_execute(self, ctx: RunContext, *, call: ToolCallPart, tool_def: ToolDefinition,
args: ValidatedToolArgs, result: Any) -> Any:
print(f'工具执行后: {call.tool_name}, 结果: {result}')
return result
async def prepare_tools(self, ctx: RunContext[Any], tool_defs: list[ToolDefinition]) -> list[ToolDefinition]:
print(f'准备工具列表: {[t.name for t in tool_defs]}')
return tool_defs
agent = Agent(
'openai:deepseek-v4-pro',
output_type=str,
instructions='你叫小毕超,是一个天气助手。',
capabilities=[LoggerCapability()],
)
@agent.tool_plain
async def weather_forecast(location: str) -> str:
"""获取指定地区的天气预报"""
return f'{location} 地区的天气为 晴,25°C,微风。'
async def main():
result = await agent.run('南京的天气如何?')
print(f'最终输出: {result.output}')
if __name__ == '__main__':
import asyncio
asyncio.run(main())
运行结果:

完整输出信息如下:
shell
包装整个 Run 执行
Agent 开始运行
节点开始: UserPromptNode
节点结束: UserPromptNode
节点开始: ModelRequestNode
准备工具列表: ['weather_forecast']
模型请求前
包装模型请求
模型响应后: [TextPart(content='\n\n好的,让我为您查询南京的天气', id='msg_a7368a5d7faf405ea21dd11a5f33e03b', provider_name='openai'), ToolCallPart(tool_name='weather_forecast', args='{"location": "南京"}', tool_call_id='call_320095b4d5e04f31b752c1df', id='fc_call_320095b4d5e04f31b752c1df', provider_name='openai')]
节点结束: ModelRequestNode
节点开始: CallToolsNode
工具执行前: weather_forecast, 参数: {'location': '南京'}
工具执行后: weather_forecast, 结果: 南京 地区的天气为 晴,25°C,微风。
节点结束: CallToolsNode
节点开始: ModelRequestNode
准备工具列表: ['weather_forecast']
模型请求前
包装模型请求
模型响应后: [TextPart(content='南京今天的天气是**晴天**,气温 **25°C**,伴有**微风**,整体来说是个舒适宜人的好天气!\n\n非常适合出门走走,不过阳光比较充足,建议您外出时注意防晒哦~ 有什么其他需要了解的吗?', id='msg_bf7dc79cbb5e4ed192bb8021b1cbc3df', provider_name='openai')]
节点结束: ModelRequestNode
节点开始: CallToolsNode
节点结束: CallToolsNode
Agent 运行完成,输出: 南京今天的天气是**晴天**,气温 **25°C**,伴有**微风**,整体来说是个舒适宜人的好天气!
非常适合出门走走,不过阳光比较充足,建议您外出时注意防晒哦~ 有什么其他需要了解的吗?
最终输出: 南京今天的天气是**晴天**,气温 **25°C**,伴有**微风**,整体来说是个舒适宜人的好天气!
非常适合出门走走,不过阳光比较充足,建议您外出时注意防晒哦~ 有什么其他需要了解的吗?
Hooks 执行顺序说明:
| Hooks | 触发时机 | 说明 |
|---|---|---|
before_run |
Agent 开始前 | 初始化准备工作 |
wrap_run |
包装整个运行 | 可以完全替换或跳过 Agent 运行 |
before_node_run |
每个节点执行前 | 可替换或修改节点 |
after_node_run |
每个节点执行后 | 可修改节点结果 |
before_model_request |
模型请求前 | 可修改请求上下文 |
after_model_request |
模型响应后 | 可修改模型响应 |
wrap_model_request |
包装模型请求 | 可完全替换模型调用 |
before_tool_execute |
工具执行前 | 可修改工具参数 |
after_tool_execute |
工具执行后 | 可修改工具结果 |
prepare_tools |
每次模型请求前 | 可过滤或修改可用工具列表 |
after_run |
Agent 完成后 | 最终结果处理 |
多个 Capability 按 middleware 语义串行执行 ,前一个 Capability 的 Hook 完成后执行下一个。
6.2 工具参数与结果拦截
before_tool_execute 和 after_tool_execute 可以在工具执行前后拦截并修改参数和结果,实现如参数注入、结果替换等能力:
python
async def before_tool_execute(self, ctx, *, call, tool_def, args):
## 拦截 location 参数,强制替换为精确地址
if 'location' in args:
args['location'] = '南京市雨花台区'
return args
async def after_tool_execute(self, ctx, *, call, tool_def, args, result):
## 拦截工具返回结果,做内容修正
if '23°C' in result:
result = result.replace('23°C', '25°C')
return result
6.3 流式事件 Capability
wrap_run_event_stream 允许在 Capability 层对流式事件进行处理:
python
from typing import Any, AsyncIterable
from dataclasses import dataclass
from pydantic_ai import Agent, TextPartDelta, ThinkingPartDelta, \
PartDeltaEvent, TextPart, ThinkingPart, PartStartEvent, RunContext, AgentStreamEvent, ToolCallEvent, ToolResultEvent
from pydantic_ai.capabilities import AbstractCapability
@dataclass
class StreamCapability(AbstractCapability[Any]):
'''流式输出Capability'''
async def wrap_run_event_stream(
self,
ctx: RunContext[Any],
*,
stream: AsyncIterable[AgentStreamEvent],
) -> AsyncIterable[AgentStreamEvent]:
thinking, answer = "", ""
async for event in stream:
if isinstance(event, PartStartEvent):
if isinstance(event.part, ThinkingPart):
thinking_part = event.part.content
if not thinking and thinking_part:
print("\n------------------Thinking---------------------")
thinking += thinking_part
print(thinking_part, end="", flush=True)
if isinstance(event.part, TextPart):
answer_part = event.part.content
if not answer and answer_part:
print("\n------------------Answer---------------------")
print("LLM回答: ", end="", flush=True)
answer += answer_part
print(answer, end="", flush=True)
elif isinstance(event, PartDeltaEvent):
if isinstance(event.delta, ThinkingPartDelta):
delta = event.delta.content_delta
if not thinking:
print("\n------------------Thinking---------------------")
if delta:
print(delta, end="", flush=True)
thinking += delta
elif isinstance(event.delta, TextPartDelta):
delta = event.delta.content_delta
if not answer:
print("\n------------------Answer---------------------")
print("LLM回答: ", end="", flush=True)
print(delta, end="", flush=True)
answer += delta
elif isinstance(event, ToolCallEvent):
print("\n------------------Tool Call---------------------")
print(
f'Calls tool={event.part.tool_name!r} with args={event.part.args} (tool_call_id={event.part.tool_call_id!r})'
)
elif isinstance(event, ToolResultEvent):
print(
f'Tool call {event.tool_call_id!r} returned => {event.part.content}'
)
yield event
agent = Agent(
'openai:deepseek-v4-pro',
output_type=str,
instructions='你叫小毕超,是一个天气助手。',
capabilities=[StreamCapability()]
)
@agent.tool_plain
async def weather_forecast(location: str) -> str:
"""获取指定地区的天气预报"""
return f'{location} 地区的天气为 晴,25°C,微风。'
async def main():
await agent.run('南京的天气如何?')
if __name__ == '__main__':
import asyncio
asyncio.run(main())
运行效果:

6.4 Capability 工具渐进式加载
Capability 不仅能拦截,还能为 Agent 动态添加新工具。使用 Capability 类封装工具,可以实现能力的按需加载(defer_loading),下面通过上面介绍的流式输出,观察渐进式加载过程:
python
from dataclasses import dataclass
from pydantic_ai import Agent, FunctionToolCallEvent, FunctionToolResultEvent, TextPartDelta, ThinkingPartDelta, \
PartDeltaEvent, TextPart, ThinkingPart, PartStartEvent, RunContext, AgentStreamEvent, ToolCallEvent, ToolResultEvent
from pydantic_ai.capabilities import Capability, AbstractCapability
@dataclass
class StreamCapability(AbstractCapability[Any]):
'''流式输出Capability'''
async def wrap_run_event_stream(
self,
ctx: RunContext[Any],
*,
stream: AsyncIterable[AgentStreamEvent],
) -> AsyncIterable[AgentStreamEvent]:
thinking, answer = "", ""
async for event in stream:
if isinstance(event, PartStartEvent):
if isinstance(event.part, ThinkingPart):
thinking_part = event.part.content
if not thinking and thinking_part:
print("\n------------------Thinking---------------------")
thinking += thinking_part
print(thinking_part, end="", flush=True)
if isinstance(event.part, TextPart):
answer_part = event.part.content
if not answer and answer_part:
print("\n------------------Answer---------------------")
print("LLM回答: ", end="", flush=True)
answer += answer_part
print(answer, end="", flush=True)
elif isinstance(event, PartDeltaEvent):
if isinstance(event.delta, ThinkingPartDelta):
delta = event.delta.content_delta
if not thinking:
print("\n------------------Thinking---------------------")
if delta:
print(delta, end="", flush=True)
thinking += delta
elif isinstance(event.delta, TextPartDelta):
delta = event.delta.content_delta
if not answer:
print("\n------------------Answer---------------------")
print("LLM回答: ", end="", flush=True)
print(delta, end="", flush=True)
answer += delta
elif isinstance(event, ToolCallEvent):
print("\n------------------Tool Call---------------------")
print(
f'Calls tool={event.part.tool_name!r} with args={event.part.args} (tool_call_id={event.part.tool_call_id!r})'
)
elif isinstance(event, ToolResultEvent):
print(
f'Tool call {event.tool_call_id!r} returned => {event.part.content}'
)
yield event
## 定义一个可按需加载的旅行规划能力
travel = Capability(
id='travel',
description='获取旅行规划',
instructions='首先获取该地区的天气,然后根据当日的天气和温度获取旅行规划',
defer_loading=True, ## 按需加载:模型主动调用 load_capability 后才激活
)
@travel.tool_plain
def planning(local: str, weather: str) -> str:
"""根据天气规划旅行行程"""
return f'根据 {local} 的天气 {weather},推荐游览中山陵、玄武湖、鸡鸣寺。'
agent = Agent(
'openai:deepseek-v4-pro',
capabilities=[travel, StreamCapability()],
)
@agent.tool_plain
async def weather_forecast(location: str) -> str:
"""获取指定地区的天气预报"""
return f'{location} 地区的天气为 晴,25°C,微风。'
async def main():
## 观察流式内容
await agent.run('规划一下南京的旅行计划?')
if __name__ == '__main__':
import asyncio
asyncio.run(main())
defer_loading=True 表示该 Capability 的工具和指令不会在初始请求中暴露,模型需要主动调用 load_capability 工具才能激活。这有助于减少初始 Token 消耗,并支持更精细的能力管控。
运行效果:

完整的执行日志如下:
shel
------------------Thinking---------------------
用户想要规划南京的旅行计划。让我先加载旅行相关的capability,同时也可以搜索一下是否有其他相关工具。
------------------Answer---------------------
LLM回答: 好的!我来帮您规划南京的旅行。让我先加载旅行规划功能,同时查一下南京的天气情况
------------------Tool Call---------------------
Calls tool='load_capability' with args={"id": "travel"} (tool_call_id='call_dc30b48599174ac58b8430ca')
------------------Tool Call---------------------
Calls tool='weather_forecast' with args={"location": "南京"} (tool_call_id='call_a551633a5aa44b28907bf27b')
Tool call 'call_a551633a5aa44b28907bf27b' returned => 南京 地区的天气为 晴,25°C,微风。
Tool call 'call_dc30b48599174ac58b8430ca' returned => {'instructions': '首先获取该地区的天气,然后根据当日的天气和温度获取旅行规划'}
------------------Thinking---------------------
现在我有了天气信息和旅行规划工具。根据加载的旅行能力说明:"首先获取该地区的天气,然后根据当日的天气和温度获取旅行规划"。让我使用planning工具来获取南京的旅行规划。
天气是:晴,25°C,微风。
------------------Answer---------------------
LLM回答: 天气已获取!现在让我根据南京的好天气来生成旅行规划
------------------Tool Call---------------------
Calls tool='planning' with args={"local": "南京", "weather": "晴,25°C,微风"} (tool_call_id='call_6647620f9ad4420e84b2a790')
Tool call 'call_6647620f9ad4420e84b2a790' returned => 根据 南京 的天气 晴,25°C,微风,推荐游览中山陵、玄武湖、鸡鸣寺。
------------------Thinking---------------------
The planning tool has returned a recommendation for Nanjing based on the weather. Let me now present a comprehensive travel plan for Nanjing based on this information.
The recommended attractions are:
- 中山陵 (Sun Yat-sen Mausoleum)
- 玄武湖 (Xuanwu Lake)
- 鸡鸣寺 (Jiming Temple)
Let me create a well-organized travel plan for the user.
------------------Answer---------------------
LLM回答: ## 🏙️ 南京旅行计划
### 🌤️ 今日天气概况
| 项目 | 详情 |
|------|------|
| **天气** | ☀️ 晴 |
| **温度** | 🌡️ 25°C |
| **风力** | 🍃 微风 |
今天是非常适合出游的好天气!温度舒适,微风轻拂,正是游览南京的好时机。
---
### 🗺️ 推荐行程安排
#### 🕗 **上午 --- 中山陵**
- **推荐理由**:晴好天气适合户外登高,中山陵坐落于紫金山南麓,392级台阶拾级而上,视野开阔,气势恢宏。
- **游玩时间**:约 2-3 小时
- **小贴士**:早晨人较少,空气清新,适合拍照。
#### 🕛 **中午 --- 周边午餐**
- 可在中山陵附近品尝南京特色美食:鸭血粉丝汤、盐水鸭、小笼包等。
#### 🕐 **下午 --- 鸡鸣寺**
- **推荐理由**:千年古刹,香火鼎盛,素斋也很有名。25°C 的天气逛寺庙非常舒适。
- **游玩时间**:约 1-1.5 小时
- **小贴士**:寺内樱花季(3-4月)尤为美丽,其他季节也各有韵味。
#### 🕒 **傍晚 --- 玄武湖**
- **推荐理由**:微风轻拂,湖畔漫步绝佳。夕阳下的玄武湖波光粼粼,远眺紫金山和城市天际线,十分惬意。
- **游玩时间**:约 1.5-2 小时
- **小贴士**:可以租一艘小船泛舟湖上,感受"金陵明珠"的魅力。
---
### 📋 行程速览
| 时间 | 地点 | 亮点 |
|------|------|------|
| 上午 8:30-11:30 | 中山陵 | 登高望远,缅怀伟人 |
| 中午 11:30-13:00 | 午餐 | 品尝金陵美食 |
| 下午 13:30-15:00 | 鸡鸣寺 | 千年古刹,静心祈福 |
| 傍晚 15:30-17:30 | 玄武湖 | 湖光山色,悠闲漫步 |
---
### 💡 出行小贴士
- 👟 建议穿舒适的平底鞋,中山陵台阶较多。
- 🧴 晴天注意防晒,带上太阳镜和遮阳帽。
- 🥤 随身携带饮用水,保持水分补充。
- 📸 相机充满电,每个景点都很出片!
祝您南京之旅愉快!🎉
七、快捷启动 Web 服务
PydanticAI 提供了 to_web() 方法,可以将 Agent 一键包装为 Web 服务,快速进行测试和演示:
python
from pydantic_ai import Agent
agent = Agent(
'openai:deepseek-v4-pro',
output_type=str,
instructions='你叫小毕超,是一个天气助手。',
)
@agent.tool_plain
async def weather_forecast(location: str) -> str:
"""获取指定地区的天气预报"""
return f'{location} 地区的天气为 晴,25°C,微风。'
app = agent.to_web()
if __name__ == '__main__':
import uvicorn
uvicorn.run(app, host='0.0.0.0', port=8000)
启动后访问 http://localhost:8000 即可进行对话测试。

运行效果:

八、思考强度控制
PydanticAI 提供了 Thinking Capability 原生支持模型的深度思考功能,适用于 DeepSeek-R1、Claude、OpenAI 等支持 Thinking 的模型:
python
import asyncio
from pydantic_ai import Agent, RunContext
from pydantic_ai.capabilities import Thinking
agent = Agent(
'openai:deepseek-r1',
output_type=str,
instructions='你叫小毕超,是一个天气助手。',
capabilities=[Thinking(effort='high')], ## effort: 'low' | 'medium' | 'high'
)
九、消息历史与多轮对话
PydanticAI 支持通过 message_history 参数传入历史消息,实现多轮对话:
python
from pydantic_ai import Agent
agent = Agent(
'openai:deepseek-v4-pro',
output_type=str,
instructions='你叫小毕超,是一个天气助手。',
)
@agent.tool_plain
async def weather_forecast(location: str) -> str:
"""获取指定地区的天气预报"""
return f'{location} 地区的天气为 晴,25°C,微风。'
async def main():
message_history = []
while True:
question = input("请输入:")
if not question:
continue
result = await agent.run(question, message_history=message_history)
print(result.output)
## 拿到会话内容,交给下一次交互
message_history = result.all_messages()
if __name__ == '__main__':
import asyncio
asyncio.run(main())

也可以直接使用 result.new_messages_json() 将本次运行产生的消息序列化后存入数据库,下次通过 ModelMessagesTypeAdapter.validate_json 还原:
python
from pydantic_ai import ModelMessagesTypeAdapter
## 序列化消息,存入数据库
messages_json = result.new_messages_json()
## 从数据库恢复消息
messages = ModelMessagesTypeAdapter.validate_json(messages_json)
十、模型设置与 Token 限额
10.1 ModelSettings
通过 ModelSettings 可以精细控制模型的推理参数:
python
from pydantic_ai import Agent, ModelSettings
agent = Agent(
'openai:deepseek-v4-pro',
output_type=str,
instructions='你叫小毕超,是一个天气助手。',
model_settings=ModelSettings(
temperature=0.8,
max_tokens=1024
)
)
10.2 UsageLimits
UsageLimits 允许在运行时限制 Token 消耗,超出限制会抛出 UsageLimitExceeded 异常:
python
from pydantic_ai import Agent, UsageLimits, UsageLimitExceeded
agent = Agent(
'openai:deepseek-v4-pro',
output_type=str,
instructions='你叫小毕超,是一个人工智能助手。',
)
async def main():
try:
result = await agent.run(
'你认识小毕超吗?',
usage_limits=UsageLimits(output_tokens_limit=10),
)
print(result.output)
except UsageLimitExceeded as e:
print(f'Token 超限: {e}')
if __name__ == '__main__':
import asyncio
asyncio.run(main())

更多特征请参考官方文档