生产级智能体框架 Pydantic AI 介绍和使用

一、PydanticAI 介绍

在本专栏前两篇文章中,我们围绕 分布式多智能体 这一核心命题展开了完整的工程实践。

第一篇基于 AgentScope + A2A + Nacos,解决了异构 Agent 之间的通信与协作难题。A2AAgent-to-Agent)协议统一了不同框架下智能体的消息格式与交互规范,Nacos 担任注册中心角色,实现了 Agent 服务的注册、发现与负载均衡,最终达到 Python 端多个 Agent 实例组成集群、客户端无感切换。文章地址如下:

基于A2A + AgentScope + Nacos 构建分布式高可用异构 Agent 方案

第二篇则将视角延伸到了 Java 生态,基于 Spring AI Alibaba 快速搭建 A2A 服务端与客户端,实现了 Java 智能体注册至 Nacos 并被 Python 客户端远程调用,打通了 JavaPython 两套技术栈之间的协作壁垒,文章地址如下:

Spring AI Alibaba + Nacos 构建 A2A 分布式智能体

然而,前两篇文章的重心在于 智能体之间如何协同 ,对于 单个 Python 智能体本身的构建 这个问题上,如何以一种更贴近 Python 开发习惯、更灵活、更生产就绪的方式来实现,还有很多的探索空间。

回到单个 Agent 的构建本身,工程上真正棘手的问题往往不是如何调用模型,而是模型的输出是不是可控,参数类型是否安全、运行时的上下文依赖能否统一管理等等,而 PydanticAI 的出现正是从这个角度切入。

PydanticAI 是由 Pydantic 团队(FastAPIPydantic 背后的团队)推出的新一代 Agent 开发框架,它的核心目标是:以类型安全、结构化、可观测的方式构建生产级 AI Agent

与其他框架不同,PydanticAI 充分利用了 Pydantic 在数据验证上的优势,将模型输入输出全程纳入类型体系管理,同时提供了完备的流式输出、工具调用、依赖注入、Capability 扩展等机制,让 Agent 开发更工程化、更易于测试和维护。

核心特性:

  • 类型安全 :基于 Pydantic 的数据验证,输入输出全程类型检查。
  • 结构化输出 :原生支持将模型输出解析为 Pydantic 模型或 Union 类型。
  • 多模型支持OpenAIAnthropicGeminiOllama 等主流模型开箱即用。
  • 依赖注入 :内置优雅的依赖注入机制,通过 deps_typeRunContext 实现运行时依赖传递。
  • 流式输出 :提供 run_streamiterstream_events 等多种流式接口。
  • Capability 扩展 :通过 AbstractCapability 实现 Agent 全生命周期 Hooks 拦截与扩展。
  • 可观测 :支持 Logfire / OpenTelemetry,工具调用、思考过程全链路可见。

官方介绍文档:https://pydantic.dev/docs/ai/overview/

二、快速上手

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:调用 OpenAIgpt-4o
  • openai:deepseek-v4-pro:通过 OpenAI 兼容接口调用 DeepSeek
  • anthropic:claude-sonnet-4-5:调用 AnthropicClaude

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 扩展机制

CapabilityPydanticAI 最强大的扩展机制,允许开发者在 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 完成后 最终结果处理

多个 Capabilitymiddleware 语义串行执行 ,前一个 CapabilityHook 完成后执行下一个。

6.2 工具参数与结果拦截

before_tool_executeafter_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-R1ClaudeOpenAI 等支持 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())

更多特征请参考官方文档

官方文档:https://pydantic.dev/docs/ai/overview

相关推荐
怕浪猫3 小时前
第14章 性能优化与成本控制
openai·agent·ai编程
MobotStone4 小时前
为什么你的智能体总在第二步就崩溃?
agent
KaneLogger17 小时前
防止 AI Agent 误删文件,最简单的办法就是加一个 Hook
llm·agent·trae
永远的沙雕网友19 小时前
写点粗浅的harness相关内容(1)
agent
为你学会写情书19 小时前
LangChain.js RAG 实战:爬取掘金文章,让 LLM 告别幻觉
agent
带刺的坐椅20 小时前
SolonCode v2026.7.13 发布:多工作区协同、推理水平可控、子代理任务分组、工程体验再进化
llm·agent·codex·solon-ai·claudecode·opencode
AI程序员21 小时前
会写代码的 AI 很多,能算 Coding Agent 的没几个
人工智能·agent
zxfeng~1 天前
伴眸-居家养老AI 眼镜智能体 技术方案
人工智能·agent·ai编程·智能眼镜
李燚1 天前
Eino devops 调试系统源码:GraphCompileCallback 怎么工作(第50篇-E36)
数据库·golang·agent·devops·graphql·aiagent·eino