This commit is contained in:
liubiren 2026-08-13 20:09:34 +08:00
parent 5b5f41be49
commit 92674c434b
1 changed files with 120 additions and 65 deletions

View File

@ -7,7 +7,7 @@ import datetime
from typing import AsyncGenerator from typing import AsyncGenerator
from logfire_api.variables import ValueDoesNotEqual from logfire_api.variables import ValueDoesNotEqual
from pydantic import BaseModel, Field, field_validator from pydantic import BaseModel, Field, field_validator, TypeAdapter
from pydantic_ai import ( from pydantic_ai import (
Agent, Agent,
ApprovalRequired, ApprovalRequired,
@ -23,6 +23,7 @@ from pydantic_ai import (
from pydantic_ai.models.openai import OpenAIChatModel from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider from pydantic_ai.providers.openai import OpenAIProvider
from sqlalchemy.sql.dml import ReturningDelete from sqlalchemy.sql.dml import ReturningDelete
from pydantic_ai.run import AgentRunResultEvent
DEEPSEEK_V4_FLASH_MODEL = OpenAIChatModel( DEEPSEEK_V4_FLASH_MODEL = OpenAIChatModel(
@ -38,17 +39,6 @@ MODEL_SETTINGS = ModelSettings(
) # 禁用思考模式 ) # 禁用思考模式
class Deps(BaseModel):
"""
依赖类
"""
date: datetime.date = Field(..., description="日期")
origin_airport_code: str = Field(..., description="出发机场代码")
destination_airport_code: str = Field(..., description="到达机场代码")
source_material: str = Field(..., description="来源资料")
class Flight(BaseModel): class Flight(BaseModel):
""" """
航班类 航班类
@ -79,18 +69,35 @@ class FlightNoFound(BaseModel):
""" """
system_prompt = """你的任务是根据用户提供的日期、出发机场和到达机场选择航班。 class Deps(BaseModel):
1. 使用 extract_flights 从来源资料提取所有航班信息 """
2. 使用 select_flight 从所有航班信息中选择匹配航班 依赖类
""" """
# 主智能体 date: datetime.date = Field(..., description="日期")
agent = Agent[Deps, Flight | FlightNoFound]( origin_airport_code: str = Field(..., description="出发机场代码")
destination_airport_code: str = Field(..., description="到达机场代码")
source_material: str = Field(..., description="来源资料")
extracted_flights: list[Flight] | None = Field(
default=None, description="提取到的所有航班信息"
)
selected_flight: Flight | FlightNoFound | None = Field(
default=None, description="查询到的航班"
)
# 航班查询与预定智能体
agent = Agent[Deps, Flight | FlightNoFound | DeferredToolRequests](
model=DEEPSEEK_V4_FLASH_MODEL, model=DEEPSEEK_V4_FLASH_MODEL,
model_settings=MODEL_SETTINGS, model_settings=MODEL_SETTINGS,
deps_type=Deps, deps_type=Deps,
output_type=Flight | FlightNoFound, output_type=Flight | FlightNoFound | DeferredToolRequests,
system_prompt=system_prompt, system_prompt=(
"你的任务是帮助用户查询并预定航班,**必须按照下述步骤执行**",
"1. 使用 extract_flights 提取所有航班信息;",
"2. 使用 select_flight 查询航班;",
"3. 若查询到航班则使用 book_flight 预定航班;若未查询到航班则返回 FlightNoFound。",
),
) )
# 航班信息提取智能体 # 航班信息提取智能体
@ -99,64 +106,78 @@ extraction_agent = Agent(
model_settings=MODEL_SETTINGS, model_settings=MODEL_SETTINGS,
output_type=list[Flight], output_type=list[Flight],
system_prompt=( system_prompt=(
"你的任务是根据给定的来源资料提取出所有航班信息,包括航班号、日期、出发机场代码、到达机场代码和机票价格。", "你的任务是根据提供的来源资料提取出所有航班信息,包括航班号、日期、出发机场代码、到达机场代码和机票价格。",
"其中出发机场代码和到达机场代码须为英文、大写、3位字符串例如 San Francisco International Airport (SFO) 的机场代码为 SFO。", "其中出发机场代码和到达机场代码须为英文、大写、3位字符串例如 San Francisco International Airport (SFO) 的机场代码为 SFO。",
"若某航班信息不全则跳过该航班,若没有任何航班信息则返回空列表。", "若某航班信息不全则跳过该航班,若没有任何航班信息则返回空列表。",
"禁止编造。", "禁止编造。",
), ),
) # 通过提示词约束输出
# 航班选择智能体
selection_agent = Agent(
model=DEEPSEEK_V4_FLASH_MODEL,
model_settings=MODEL_SETTINGS,
output_type=Flight | FlightNoFound,
system_prompt="你的任务是从所有航班信息中选择匹配用户提供的日期、出发机场和到达机场的航班:若有多个航班则选择最便宜的航班,选择完成需用户确认是否预定;若没有则返回 FlightNoFound。",
) )
"""
如何提高提取准确率
1. 系统提示词规则约束
2. 降低模型温度
3. 输出类型约定和输出模型校验
4. 业务规则约束例如输出不可为空列表
"""
@agent.tool @agent.tool
async def extract_flights(ctx: RunContext[Deps]) -> list[Flight]: async def extract_flights(ctx: RunContext[Deps]) -> list[Flight]:
""" """
提取所有航班信息 提取所有航班信息
**如何提高提取准确率**
1. 系统提示词规则约束
2. 降低模型温度
3. 输出类型约定和输出模型校验
4. 业务规则约束例如输出不可为空列表
""" """
result = await extraction_agent.run( result = await extraction_agent.run(
ctx.deps.source_material, ctx.deps.source_material,
usage_limits=UsageLimits(request_limit=3),
usage=ctx.usage, usage=ctx.usage,
usage_limits=UsageLimits(request_limit=10),
) )
if len(output := result.output) != 8:
raise ModelRetry("提取到的所有航班信息应为 8 条") if (flight_counts := len(output := result.output)) != 8: # 模拟业务规则约束
raise ModelRetry(f"所有航班信息数量应为 8 ,不是 {flight_counts}")
# 更新提取到的航班信息
ctx.deps.extracted_flights = output
return output return output
# 航班查询智能体
selection_agent = Agent[Deps, Flight | FlightNoFound](
model=DEEPSEEK_V4_FLASH_MODEL,
model_settings=MODEL_SETTINGS,
deps_type=Deps,
output_type=Flight | FlightNoFound,
system_prompt=(
"你的任务是在提取到的所有航班信息中查询匹配用户提供的日期、出发机场代码和到达机场代码的机票价格最低的航班。",
"若没有符合要求的航班则返回 FlightNoFound。",
),
)
@agent.tool @agent.tool
async def select_flight(ctx: RunContext[Deps]) -> Flight | FlightNoFound: async def select_flight(ctx: RunContext[Deps]) -> Flight | FlightNoFound:
""" """
选择航班 查询航班
""" """
result = await selection_agent.run(ctx.deps.source_material, usage=ctx.usage) extracted_flights = ctx.deps.extracted_flights
if isinstance(result.output, Flight): if not extracted_flights:
if not ctx.tool_call_approved: raise ModelRetry("须先使用 extract_flights 提取所有航班信息")
raise ApprovalRequired(metadata={"reason": "protected"})
return result.output result = await selection_agent.run(
(
f"{ctx.prompt}",
f"所有航班信息:\n{TypeAdapter(list[Flight]).dump_json(extracted_flights).decode('utf-8')}",
),
deps=ctx.deps,
usage=ctx.usage,
usage_limits=UsageLimits(request_limit=10),
)
ctx.deps.selected_flight = (output := result.output)
return output
@agent.output_validator @selection_agent.output_validator
async def validate_output( async def validate_output(
ctx: RunContext[Deps], output: Flight | FlightNoFound ctx: RunContext[Deps], output: Flight | FlightNoFound
) -> Flight | FlightNoFound: ) -> Flight | FlightNoFound:
""" """
输出校验 航班查询智能体输出校验
""" """
# 不校验未查询到航班 # 不校验未查询到航班
if isinstance(output, FlightNoFound): if isinstance(output, FlightNoFound):
@ -165,17 +186,39 @@ async def validate_output(
errors = [] errors = []
if output.date != ctx.deps.date: if output.date != ctx.deps.date:
errors.append(f"航班日期应为 {ctx.deps.date}, 不是 {output.date}") errors.append(f"航班日期应为 {ctx.deps.date}, 不是 {output.date}")
if output.origin != ctx.deps.origin: if output.origin_airport_code != ctx.deps.origin_airport_code:
errors.append(f"航班出发机场应为 {ctx.deps.origin}, 不是 {output.origin}")
if output.destination != ctx.deps.destination:
errors.append( errors.append(
f"航班到达机场应为 {ctx.deps.destination}, 不是 {output.destination}" f"航班出发机场应为 {ctx.deps.origin_airport_code}, 不是 {output.origin_airport_code}"
)
if output.destination_airport_code != ctx.deps.destination_airport_code:
errors.append(
f"航班到达机场应为 {ctx.deps.destination_airport_code}, 不是 {output.destination_airport_code}"
) )
if errors: if errors:
raise ModelRetry("\n".join(errors)) raise ModelRetry("\n".join(errors))
return output return output
@agent.tool
async def book_flight(ctx: RunContext[Deps]) -> Flight | FlightNoFound:
"""
预定航班
"""
if not (selected_flight := ctx.deps.selected_flight):
raise ModelRetry("须先使用 select_flight 查询航班")
if isinstance(selected_flight, FlightNoFound):
return selected_flight
else:
if not ctx.tool_call_approved:
raise ApprovalRequired(
metadata={
"reason": f"需要用户确认是否预定 {selected_flight.number}{selected_flight.date.strftime('%Y-%m-%d')}{selected_flight.origin_airport_code}{selected_flight.destination_airport_code} 的航班"
}
)
return selected_flight
source_material = """ source_material = """
1. Flight SFO-AK123 1. Flight SFO-AK123
- Price: $350 - Price: $350
@ -221,8 +264,8 @@ source_material = """
async def run_stream_events( async def run_stream_events(
user_prompt: str,
deps: Deps, deps: Deps,
user_prompt: str,
message_history: list[ModelMessage], message_history: list[ModelMessage],
usage: RunUsage, usage: RunUsage,
deferred_tool_results: DeferredToolResults | None = None, deferred_tool_results: DeferredToolResults | None = None,
@ -239,30 +282,42 @@ async def run_stream_events(
async def main(): async def main():
# 实例化依赖
deps = Deps( deps = Deps(
source_material=source_material, source_material=source_material,
date=datetime.date(2025, 1, 10), date=datetime.date(2025, 1, 10),
origin="SFO", origin_airport_code="SFO",
destination="ANC", destination_airport_code="ANC",
selected_flight=None,
) )
# message_history 传空列表,不要传 None user_prompt = f"帮我查询并预定在 {deps.date.strftime('%Y-%m-%d')}{deps.origin_airport_code}{deps.destination_airport_code} 的航班"
msg_history: list[ModelMessage] = [] message_history: list[ModelMessage] = []
prompt = f"Find me a flight from {deps.origin} to {deps.destination} on {deps.date}" events = run_stream_events(
gen = run_stream_events(
user_prompt=prompt,
deps=deps, deps=deps,
message_history=msg_history, user_prompt=user_prompt,
message_history=message_history,
usage=RunUsage(), usage=RunUsage(),
) )
# 消费异步生成器 # 消费异步生成器
async for ev in gen: async for event in events:
print(ev) print(event)
if isinstance(event, AgentRunResultEvent):
# 保存本次运行新增消息
message_history.extend(event.result.new_messages())
asyncio.run(main()) asyncio.run(main())
""" """
@selection_agent.tool
async def select_flight(ctx: RunContext[Deps]) -> Flight | FlightNoFound:
result = await selection_agent.run(ctx.deps.source_material, usage=ctx.usage)
if isinstance(result.output, Flight):
if not ctx.tool_call_approved:
raise ApprovalRequired(metadata={"reason": "protected"})
return result.output
if not isinstance(event, AgentRunResultEvent): if not isinstance(event, AgentRunResultEvent):
yield event yield event
else: else: