AI Agent Workflow(可以使用不支持functionCall(也叫toolCall)的大模型)
·
代码(需要修改调试)
父类
import json
from dataclasses import dataclass, field
from typing import Annotated, Sequence, Literal, Dict, List, TypeAlias, Any, TypedDict
from typing_extensions import TypedDict
from langchain_core.tools import tool
from langgraph.graph import StateGraph
from langgraph.prebuilt import ToolNode
from langgraph.graph.message import add_messages
from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage, AIMessage
from langchain_core.runnables import RunnableConfig
from langgraph.managed import IsLastStep
AnyMessage: TypeAlias = BaseMessage
@dataclass
class InputState:
"""输入状态类"""
messages: Annotated[Sequence[AnyMessage], add_messages] = field(
default_factory=list
)
@dataclass
class State(InputState):
"""扩展的状态类"""
is_last_step: IsLastStep = field(default=False)
class BaseAgent:
"""Agent 基础类,封装了所有公共方法和属性"""
# 默认的 ReAct 提示模板
REACT_PROMPT = '''
你被设计用于帮助完成各种输入任务,包括回答问题、内容创作、自动化处理等。
## 工具
你可以使用各种工具,并且需要自行决定使用工具的顺序,以完成当前任务。
这可能需要将任务拆分为多个子任务,并使用不同的工具来完成各个子任务。
你可以使用以下工具:
{tools_desc}
## 输出格式
如果本次需要使用工具完成某个子任务,请按照以下格式输出:
Thought: 我需要使用一个工具来帮助回答这个问题。
Action: 工具名称 (从 {tool_names} 中选择一个工具),如果需要使用工具的话。
Action Input: 传递给工具的输入,使用 JSON 格式表示参数(例如:{{"query": "你好"}})。
注意:
* 始终以“Thought”开头。
* 绝对不要在你的响应外部使用 Markdown 代码标记,但如果在你的回答中需要,你可以在适当的位置使用代码标记。
* 请使用有效的JSON 格式作为Action Input。不要使用类似 {{'input': '你好世界', 'num_beams': 5}} 这种错误格式。
* 一次响应最多只能使用一个工具以完成一个子任务。不要在一次响应中出现多个Action。
如果你已经获得足够的信息而来输出最终回答,则必须使用以下两种格式之一来输出最后答案:
Thought: 我可以在不使用更多工具的情况下回答问题。
Answer: [你的回答 (与用户问题的语言相同)]
Thought: 我无法使用提供的工具回答该问题。
Answer: [你的回答 (与用户问题的语言相同)]
注意:
* 确保所有子任务都完成后才输出最终答案
* 最终回答尽量涵盖用户任务的所有方面
## 当前对话
以下是当前的对话历史, 由人类、AI的消息交替组成。
'''
def __init__(self):
"""初始化方法"""
self.llm = None # 子类需要设置具体的LLM
self.tools = [] # 子类需要设置具体的工具
self.graph = None # 工作流图
async def call_model(self, state: State, config: RunnableConfig) -> Dict[str, List[AIMessage]]:
"""调用模型的核心方法"""
# 生成工具描述
tools_desc = "\n".join([
f"- {tool.name}: {tool.description}\n"
for tool in self.tools
])
tool_names = [tool.name for tool in self.tools]
# 生成 ReAct 提示
system_prompt = self.REACT_PROMPT.format(
tools_desc=tools_desc,
tool_names=tool_names
)
# 预处理消息
state.messages = self.preprocess_messages(state.messages)
# 调用 LLM
response = await self.llm.ainvoke([SystemMessage(content=system_prompt)] + state.messages)
content = response.content
print("\n===========================Reasoning================================")
print(f'{content}')
print("=========================Reasoning End================================\n")
# 处理工具调用
if "Action:" in content and "Action Input:" in content:
action_lines = [line for line in content.split('\n') if
line.startswith('Action:') or line.startswith('Action Input:')]
tool_name = action_lines[0].replace('Action:', '').strip()
tool_input = action_lines[1].replace('Action Input:', '').replace("'", '"').strip()
tool_input = tool_input.replace('\\', '\\\\')
response.tool_calls = [{
"id": "call_1",
"type": "function",
"name": tool_name,
"args": json.loads(tool_input)
}]
# 处理最后一步
if state.is_last_step and response.tool_calls:
return {
"messages": [
AIMessage(
content="对不起,我在指定的步骤数内无法找到问题的答案。"
)
]
}
return {"messages": [response]}
def route_model_output(self, state: State) -> Literal["__end__", "tools"]:
"""路由模型输出决定下一步"""
last_message = state.messages[-1]
if not isinstance(last_message, AIMessage):
raise ValueError(
f"Expected AIMessage in output edges, but got {type(last_message).__name__}"
)
if not last_message.tool_calls:
return "__end__"
return "tools"
def preprocess_messages(self, messages):
"""预处理消息,子类可以重写此方法"""
return messages
async def run(self, input_messages):
"""运行Agent的入口方法"""
if not self.graph:
self.initialize_graph()
return await self.graph.ainvoke({"messages": input_messages})
async def main(self, input_text):
"""主方法,方便测试"""
messages = [HumanMessage(content=input_text)]
result = await self.run(messages)
# print(f"final answer : {result[-1].content}")
return result
基类
import json
from agents.BaseAgent import BaseAgent, State, InputState
from langchain_ollama import ChatOllama
from langchain_core.messages import ToolMessage, HumanMessage, SystemMessage, AIMessage
from langchain_core.tools import tool
from typing import List, Dict, Any
import json
from dataclasses import dataclass, field
from typing import Annotated, Sequence, Literal, Dict, List, TypeAlias, Any, TypedDict
from typing_extensions import TypedDict
from langchain_core.tools import tool
from langgraph.graph import StateGraph
from langgraph.prebuilt import ToolNode
from langgraph.graph.message import add_messages
from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage, AIMessage
from langchain_core.runnables import RunnableConfig
from langgraph.managed import IsLastStep
from langchain_core.messages import ToolMessage
AnyMessage: TypeAlias = BaseMessage
class ReActAgent(BaseAgent):
"""ReAct智能体实现,继承自BaseAgent"""
# 覆盖父类的REACT_PROMPT
REACT_PROMPT = '''
你被设计用于帮助完成各种输入任务,包括回答问题、内容创作、自动化处理等。
## 工具
你可以使用各种工具,并且需要自行决定使用工具的顺序,以完成当前任务。
这可能需要将任务拆分为多个子任务,并使用不同的工具来完成各个子任务。
你可以使用以下工具:
{tools_desc}
## 输出格式
如果本次需要使用工具完成某个子任务,请按照以下格式输出:
Thought: 我需要使用一个工具来帮助回答这个问题。
Action: 工具名称 (从 {tool_names} 中选择一个工具),如果需要使用工具的话。
Action Input: 传递给工具的输入,使用 JSON 格式表示参数(例如:{{"query": "你好"}})。
注意:
* 一次响应最多只能使用一个工具以完成一个子任务。不要在一次响应中出现多个Action。
* 始终以“Thought”开头。
* 绝对不要在你的响应外部使用 Markdown 代码标记,但如果在你的回答中需要,你可以在适当的位置使用代码标记。
* 请使用有效的JSON 格式作为Action Input。不要使用类似 {{'input': '你好世界', 'num_beams': 5}} 这种错误格式。
* 一次响应最多只能使用一个工具以完成一个子任务。不要在一次响应中出现多个Action。
如果你已经获得足够的信息而来输出最终回答,则必须使用以下两种格式之一来输出最后答案:
Thought: 我可以在不使用更多工具的情况下回答问题。
Answer: [你的回答 (与用户问题的语言相同)]
Thought: 我无法使用提供的工具回答该问题。
Answer: [你的回答 (与用户问题的语言相同)]
注意:
* 确保所有子任务都完成后才输出最终答案
* 最终回答尽量涵盖用户任务的所有方面
## 当前对话
以下是当前的对话历史, 由人类、AI的消息交替组成。
'''
def __init__(self):
"""初始化ReActAgent"""
super().__init__()
# 设置Ollama模型
self.llm = ChatOllama(
model="qwen2:7b",
temperature=0.7,
repeat_penalty=1.2,
top_k=50,
top_p=0.9,
base_url="http://localhost:11434"
)
# 设置工具集
self.tools = [
self.network_search,
self.query_database,
self.document_summarizer,
self.email
]
# 初始化工作流图
self.initialize_graph()
def initialize_graph(self):
"""初始化工作流图"""
builder = StateGraph(State, input=InputState)
builder.add_node("agent", self.call_model)
builder.add_node("tools", ToolNode(self.tools))
builder.add_edge("__start__", "agent")
# 添加条件边
builder.add_conditional_edges(
"agent",
self.route_model_output,
)
builder.add_edge("tools", "agent")
# 编译图
self.graph = builder.compile()
self.graph.name = self.__class__.__name__
return self.graph
async def call_model(self, state: State, config: RunnableConfig) -> Dict[str, List[AIMessage]]:
"""调用模型的核心方法"""
# 生成工具描述
tools_desc = "\n".join([
f"- {tool.name}: {tool.description}\n"
for tool in self.tools
])
tool_names = [tool.name for tool in self.tools]
# 生成 ReAct 提示
system_prompt = self.REACT_PROMPT.format(
tools_desc=tools_desc,
tool_names=tool_names
)
# 预处理消息
state.messages = self.preprocess_messages(state.messages)
# state.messages = [HumanMessage(content=msg.content) if "ToolMessage" in msg.__class__.__name__ else msg for msg
# in
# state.messages]
# 调用 LLM
response = await self.llm.ainvoke([SystemMessage(content=system_prompt)] + state.messages)
content = response.content
print("\n===========================Reasoning================================")
print(f'{content}')
print("=========================Reasoning End================================\n")
if "Action:" in content and "Action Input:" in content:
action_lines = [line for line in content.split('\n') if
line.startswith('Action:') or line.startswith('Action Input:')]
tool_name = action_lines[0].replace('Action:', '').strip()
tool_input = action_lines[1].replace('Action Input:', '').strip()
# 修复1:移除Python格式字符串标记
tool_input = tool_input.replace("f'", "'").replace('f"', '"')
# 修复2:处理中文特殊字符
tool_input = tool_input.replace('《', '\\"').replace('》', '\\"')
# 修复3:单引号转双引号
tool_input = tool_input.replace("'", '"')
# 修复4:转义特殊字符
tool_input = tool_input.replace('\n', '\\n').replace('\t', '\\t')
try:
# 修复5:安全解析
tool_args = json.loads(tool_input)
except json.JSONDecodeError:
# 修复6:回退方案 - 创建基本结构
tool_args = {"input": tool_input}
response.tool_calls = [{
"id": "call_" + str(len(state.messages) + 1),
"type": "function",
"name": tool_name,
"args": tool_args
}]
else:
print(f"未解析到工具")
# 处理最后一步
if state.is_last_step and response.tool_calls:
return {
"messages": [
AIMessage(
content="对不起,我在指定的步骤数内无法找到问题的答案。"
)
]
}
return {"messages": [response]}
def route_model_output(self, state: State) -> Literal["__end__", "tools"]:
"""路由模型输出决定下一步"""
last_message = state.messages[-1]
if not isinstance(last_message, AIMessage):
raise ValueError(
f"Expected AIMessage in output edges, but got {type(last_message).__name__}"
)
if not last_message.tool_calls:
return "__end__"
return "tools"
def initialize_graph(self):
"""初始化工作流图"""
builder = StateGraph(State, input=InputState)
builder.add_node("agent", self.call_model)
builder.add_node("tools", ToolNode(self.tools))
builder.add_edge("__start__", "agent")
# 添加条件边
builder.add_conditional_edges(
"agent",
self.route_model_output,
)
builder.add_edge("tools", "agent")
# 编译图
self.graph = builder.compile()
self.graph.name = self.__class__.__name__
return self.graph
def preprocess_messages(self, messages):
"""重写预处理方法,转换ToolMessage为HumanMessage"""
return [
HumanMessage(content=msg.content)
if isinstance(msg, ToolMessage)
else msg
for msg in messages
]
# 定义4个模拟工具
@tool
def network_search(query: str) -> str:
"""
用于执行网络搜索并返回搜索结果
Args:
query (str): 要进行网络搜索的关键词
"""
print(f'调用tool: network_search, 输入: {query}', 'green')
try:
# 调用 TavilySearchResults 来执行搜索,模拟返回结果
# results = TavilySearchResults(max_results=2).invoke({"query": query})
# results = "哪吒票房80亿,成本30亿,上映日期2025年10月1日!"
results = "哪吒票房80亿,成本30亿"
return f"搜索结果: {results}"
except Exception as e:
return f"搜索错误: {str(e)}"
@tool
def query_database(query: str) -> str:
"""
用来查询公司数据库获取电影的投放广告费用,Args只能包含字符串
Args:
query (str): 要查询的电影名称,比如:哪吒2
"""
print(f'调用tool: query_database, 输入: {query}', 'green')
return "300万"
@tool
def document_summarizer(text: str) -> str:
"""
用来提炼与总结文本的核心内容,形成摘要
Args:
text (str): 要总结的文本内容
"""
print(f'调用tool: document_summarizer, 输入: {text}', 'green')
return "总结结果:完美"
@tool
def email(recipient, subject, body) -> str:
"""
用于发送电子邮件
Args:
recipient (str): 收件人的电子邮件地址
subject (str): 电子邮件的主题
body (str): 电子邮件的正文
"""
print(f'调用tool: email, 输入: {recipient}', 'green')
return "邮件已经发送给小明"
# 测试代码
# async def main():
# agent = ReActAgent()
# result = await agent.main("搜索《[哪吒2](@replace=10001)》的最新票房,并计算利润。最后结果发邮件到test@gmail.com")
# print(f"最终结果: {result}")
#
#
# if __name__ == "__main__":
# import asyncio
#
# asyncio.run(main())
async def main():
# 输入消息
# input_message = "搜索《哪吒2》的最新票房,并计算利润。最后结果发邮件到test@gmail.com"
# messages = [HumanMessage(content=input_message)]
agent = ReActAgent()
result = await agent.main("搜索《哪吒2》的最新票房,然后查询数据库获取我们公司给哪吒2投放了多少钱的广告,然后将票房和广告费汇总结果发邮件到test@gmail.com")
# print(f"最终结果: {result}")
print("\n=== 工作流执行完成 ===")
# 1. 查找并显示最终答案
final_answer = None
for message in reversed(result["messages"]):
if isinstance(message, AIMessage):
# 查找明确标记为"Answer"的内容
if "Answer:" in message.content:
# 提取Answer之后的内容
final_answer = message.content.split("Answer:", 1)[-1].strip()
break
elif not message.tool_calls: # 最后一条非工具调用的AI消息
final_answer = message.content
break
if final_answer:
print(f"最终结果: {final_answer}")
else:
print("未能找到明确的最终答案")
# 2. 检查工具调用结果
print("\n工具调用历史:")
for message in result["messages"]:
if isinstance(message, ToolMessage):
# 打印工具调用返回的结果
print(f"工具 '{message.tool_call_id}': {message.content}")
# 3. 检查邮件发送状态(特殊处理)
email_sent = False
for message in result["messages"]:
if isinstance(message, ToolMessage) and "邮件已" in message.content:
email_sent = True
# 提取收件人信息(假设邮件工具返回内容中有邮箱)
if "@" in message.content:
email_parts = message.content.split("邮件已发送给")[-1].split(",")
recipient = email_parts[0] if email_parts else "未知"
else:
recipient = "test@gmail.com"
print(f"\n邮件已成功发送到: {recipient}")
if not email_sent:
print("\n警告: 未检测到邮件发送结果")
return final_answer
# 运行主函数
if __name__ == "__main__":
import asyncio
asyncio.run(main())
更多推荐



所有评论(0)