feat:完成重构和测试

This commit is contained in:
2026-10-02 08:47:24 -07:00
parent 5b1e6c5654
commit 62a5b2a8f4
15 changed files with 138 additions and 43 deletions
+26 -9
View File
@@ -10,7 +10,7 @@ from typing import Literal, Self
from langchain.agents import create_agent
from langchain.agents.middleware import ModelCallLimitMiddleware, wrap_model_call
from langchain.agents.structured_output import ToolStrategy
from langchain_core.messages import AIMessage
from langchain_core.messages import AIMessage, HumanMessage
from loguru import logger
from pydantic import BaseModel, Field, model_validator
@@ -62,21 +62,36 @@ class AgentReply(BaseModel):
return self
# 偶发的空回复最多重新调用几次
EMPTY_REPLY_RETRIES = 2
# 模型没有调用工具时最多纠正几次
NO_TOOL_CALL_RETRIES = 2
NO_TOOL_CALL_HINT = (
"每一轮都要调用工具。要结束时调用 AgentReply,给用户的话写在 message 里"
)
@wrap_model_call
async def retry_empty_reply(request, handler):
"""每一轮都必须调用工具(最后一轮是 AgentReply);模型偶尔返回既没有文本也没有工具调用的空回复,
这是调用失败而不是模型的决定,重新调用即可"""
async def require_tool_call(request, handler):
"""ToolStrategy 要求每一轮都调用工具,结束时调用 AgentReply;模型偶尔只回文本(想直接回复用户)或空回复
原样重试往往得到同样的结果,或者模型转而重复提交 apply_actions 直到调用次数用尽,
所以把它的回复和纠正说明加在下一次调用里;只用于这一次调用,不写进 agent 的消息
"""
response = await handler(request)
for _ in range(EMPTY_REPLY_RETRIES):
for _ in range(NO_TOOL_CALL_RETRIES):
message = response.result[0] if response.result else None
if not isinstance(message, AIMessage) or message.tool_calls:
break
logger.warning(
f"模型返回了没有工具调用的回复,重新调用: {message.response_metadata}"
f"模型没有调用工具,纠正后重新调用: {str(message.content)[:200]}"
)
# 空回复不放回去,部分接口不接受内容为空的助手消息
replied = [message] if message.content else []
request = request.override(
messages=[
*request.messages,
*replied,
HumanMessage(content=NO_TOOL_CALL_HINT),
]
)
response = await handler(request)
return response
@@ -93,12 +108,13 @@ async def get_screen_agent():
model=get_llm(),
tools=[get_nodes, apply_actions, *await get_mcp_tools()],
system_prompt=SYSTEM_PROMPT,
# 用工具调用给出结论:试过原生结构化输出(ProviderStrategy),歧义时模型更常直接执行而不追问,评测多删了组件
response_format=ToolStrategy(AgentReply),
state_schema=ScreenAgentState,
context_schema=ScreenAgentContext,
middleware=[
*skill_middleware(),
retry_empty_reply,
require_tool_call,
# 超过上限时插入说明并结束,没有结构化结果,由节点按失败处理
ModelCallLimitMiddleware(
run_limit=settings.max_agent_steps, exit_behavior="end"
@@ -108,4 +124,5 @@ async def get_screen_agent():
checkpointer=False,
name="screen_agent",
)
return _agent