代码
import os
import functools
import operator
import getpass
from typing import Annotated, Sequence, Literal
from typing_extensions import TypedDict
from dotenv import load_dotenv
from langchain_core.messages import BaseMessage, HumanMessage, ToolMessage, AIMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langgraph.graph import END, StateGraph, START
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_experimental.tools import PythonREPLTool
from langgraph.prebuilt import ToolNode, create_react_agent
from langchain_community.chat_models.tongyi import ChatTongyi
from pydantic import BaseModel
from langchain_community.chat_models.tongyi import ChatTongyi
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
# 加载 .env 文件
load_dotenv()
# 读取环境变量
api_key = os.getenv("DASHSCOPE_API_KEY")
if not api_key:
raise ValueError("API Key not found in .env file.")
# 初始化 LLM(ChatTongyi)
llm = ChatTongyi(model='qwen-plus')
class State(TypedDict):
messages: Annotated[Sequence[BaseMessage], operator.add]
def create_agent(llm,system_message:str,name:str):
# 合并系统消息
system_prompt = (
f"你的名字是{name},你是一个AI辩论专家,现在正在参加一个两个AI的辩论。"
f"你的辩论议题为:{system_message}。"
"如果你是第一个发言的,也就是听到人类说 '请开始辩论' 的第一个发言的,你就先就你的观点发言;"
"如果你是第二个发言的或者第一轮之后发言的,就就对方的观点进行反驳。对方的发言在历史记录中有。"
"只有当你觉得自己辩论不过对方的时候,请输出 '我辩论失败'。"
)
prompt= ChatPromptTemplate.from_messages([
(
"system",system_prompt
),
MessagesPlaceholder(variable_name="messages"),
])
prompt = prompt.partial(system_message=system_message)
prompt = prompt.partial(name = name)
return prompt | llm
def agent_node(state,agent,name):
print(f"agent_node agent \n {agent}")
print(f"agent_node state \n {state}")
result = agent.invoke(state)
result = AIMessage(**result.dict(exclude={"type","name"}),name=name)
return {
"messages": [result]
}
def router(state):
print(f"router state \n {state}")
messages = state["messages"]
last_message = messages[-1]
if "我辩论失败" in last_message:
return END
return "continue"
system_message1 = "人类是否应该在火星上建立殖民地?你的观点是人类应该在火星上建立殖民地。"
system_message2 = "人类是否应该在火星上建立殖民地?你的观点是人类不应该在火星上建立殖民地。"
agent1 = create_agent(llm,system_message1,"正方辩手")
agent2 = create_agent(llm,system_message2,"反方辩手")
node1 = functools.partial(agent_node,agent=agent1,name="正方辩手")
node2 = functools.partial(agent_node,agent=agent2,name="反方辩手")
work_flow = StateGraph(State)
work_flow.add_node("node1",node1)
work_flow.add_node("node2",node2)
work_flow.add_edge(START,"node1")
work_flow.add_conditional_edges("node1",router,{"continue":"node2",END:END})
work_flow.add_conditional_edges("node2",router,{"continue":"node1",END:END})
graph = work_flow.compile()
events = graph.stream(
{
"messages": [
HumanMessage(
content="请开始辩论"
)
],
},
# Maximum number of steps to take in the graph
{"recursion_limit": 150},
)
for s in events:
print(s)
print("----")
概述
您的代码设置了一个AI辩论系统,涉及两个AI代理(正方辩手和反方辩手)在指定议题上进行辩论。系统通过状态图(StateGraph)管理辩论流程,两个代理轮流发言,直到某一方输出“我辩论失败”来结束辩论。
在执行过程中,代码通过打印语句输出了多个关键信息,帮助您跟踪系统的状态和代理的行为。以下是对这些打印内容的详细解析。
打印内容解析
1. agent_node 函数的打印输出
每当agent_node函数被调用时,它会打印两个主要部分:
a. 代理(Agent)的详细信息
agent_node agent
first=ChatPromptTemplate(input_variables=['messages'], input_types={'messages': list[typing.Annotated[typing.Union[...]]]}, partial_variables={'system_message': '...', 'name': '正方辩手'}, messages=[SystemMessagePromptTemplate(prompt=PromptTemplate(...)), MessagesPlaceholder(variable_name='messages')]) middle=[] last=ChatTongyi(client=<class 'dashscope.aigc.generation.Generation'>, model_name='qwen-plus', model_kwargs={}, dashscope_api_key=SecretStr('**********'))
分析:
-
ChatPromptTemplate:-
input_variables: 该模板接受一个名为messages的输入变量,这是辩论历史记录的集合。 -
- 定义了
input_typesmessages的类型为一个注释(
Annotated)的列表,列表中的元素可以是多种消息类型的联合(
Union),包括:
AIMessageHumanMessageChatMessageSystemMessageFunctionMessageToolMessage- 以及它们的分块版本(如
AIMessageChunk等)
-
partial_variables: 部分变量填充了system_message和name,用于动态生成系统提示。 -
- 包含两个部分:
messagesSystemMessagePromptTemplate: 系统消息模板,定义了AI代理的角色和辩论规则。MessagesPlaceholder: 一个占位符,用于插入辩论历史记录。
-
-
ChatTongyi:client: 指定了使用的生成类。model_name: 使用的模型名称为qwen-plus。dashscope_api_key: API密钥被隐藏,确保安全性。
总结:这部分打印输出展示了代理的配置,包括提示模板、模型信息和API密钥等。这有助于确认代理是否正确配置。
b. 当前状态(State)的详细信息
agent_node state
{'messages': [HumanMessage(content='请开始辩论', additional_kwargs={}, response_metadata={})]}
分析:
-
- 当前的状态是一个字典,包含一个键
statemessages,其值是一个消息列表。
-
- 表示人类发起的消息,内容为“请开始辩论”。
HumanMessagecontent: 消息内容。additional_kwargs: 额外的关键字参数,当前为空。response_metadata: 响应的元数据,当前为空。
总结:这部分打印输出展示了当前的对话状态,即已有的消息记录。在第一次调用时,仅包含人类发起的“请开始辩论”消息。
2. router 函数的打印输出
每当router函数被调用时,它会打印当前的状态,以决定辩论是否继续或结束。
router state
{'messages': [HumanMessage(content='请开始辩论', additional_kwargs={}, response_metadata={}), AIMessage(content='非常感谢主持人,各位观众朋友们,大家好!...', additional_kwargs={}, response_metadata={'model_name': 'qwen-plus', ...}, name='正方辩手', id='...')]}
分析:
-
- 类似于
stateagent_node函数中的状态字典,但此时包含了人类消息和AI代理的回应。
-
- 表示AI代理(正方辩手)的回应。
AIMessage-
content: AI的回应内容(具体内容在您的日志中被省略)。 -
additional_kwargs: 额外的关键字参数,当前为空。 -
- 响应的元数据,包括:
response_metadatamodel_name: 使用的模型名称。finish_reason: 响应完成的原因,如stop。request_id: 请求ID,用于追踪。token_usage: 令牌使用情况,包含input_tokens、output_tokens和total_tokens。
-
name: AI代理的名称(正方辩手)。 -
id: 消息的唯一标识符。
总结:这部分打印输出展示了AI代理的回应,包含了丰富的元数据信息,有助于调试和追踪请求。
3. 更新后的节点状态
在agent_node和router函数之后,代码打印了更新后的节点状态:
{'node1': {'messages': [AIMessage(content='非常感谢主持人,各位观众朋友们,大家好!...', ...)]}}
----
分析:
-
- 表示正方辩手的节点。
node1messages: 当前节点的消息列表,包含AI代理的回应。
总结:这部分打印输出展示了特定节点(如node1)的当前消息状态,有助于确认消息是否正确添加到状态中。
4. 完整的事件流示例
以下是一个完整的事件流示例,展示了多个回合的辩论过程:
agent_node agent
first=ChatPromptTemplate(...)
middle=[]
last=ChatTongyi(...)
agent_node state
{'messages': [HumanMessage(content='请开始辩论', ...)]}
router state
{'messages': [HumanMessage(...), AIMessage(content='非常感谢主持人,...', ...)]}
{'node1': {'messages': [AIMessage(content='非常感谢主持人,...', ...)]}}
----
agent_node agent
first=ChatPromptTemplate(...)
middle=[]
last=ChatTongyi(...)
agent_node state
{'messages': [HumanMessage(...), AIMessage(content='非常感谢主持人,...', ...)]}
router state
{'messages': [HumanMessage(...), AIMessage(content='非常感谢主持人,...', ...), AIMessage(content='非常感谢主持人,...', ...)]}
{'node2': {'messages': [AIMessage(content='非常感谢主持人,...', ...)]}}
----
...
分析:
- 多轮辩论:每个回合涉及
agent_node函数调用(对应不同的代理节点),打印代理和状态信息。 - 消息累积:随着辩论的进行,
messages列表不断累积,包含人类消息和各AI代理的回应。 - 节点更新:每轮结束后,节点(
node1或node2)的消息列表被更新,并通过print(s)输出。
总结:这个示例展示了辩论的动态过程,如何通过状态图管理消息的流动和节点的更新。
数据结构和格式详解
为了更好地理解打印内容中的数据结构,以下是关键部分的详细解释:
1. ChatPromptTemplate
结构:
ChatPromptTemplate(
input_variables=['messages'],
input_types={
'messages': list[
typing.Annotated[
typing.Union[
typing.Annotated[AIMessage, Tag(tag='ai')],
typing.Annotated[HumanMessage, Tag(tag='human')],
typing.Annotated[ChatMessage, Tag(tag='chat')],
typing.Annotated[SystemMessage, Tag(tag='system')],
typing.Annotated[FunctionMessage, Tag(tag='function')],
typing.Annotated[ToolMessage, Tag(tag='tool')],
typing.Annotated[AIMessageChunk, Tag(tag='AIMessageChunk')],
typing.Annotated[HumanMessageChunk, Tag(tag='HumanMessageChunk')],
typing.Annotated[ChatMessageChunk, Tag(tag='ChatMessageChunk')],
typing.Annotated[SystemMessageChunk, Tag(tag='SystemMessageChunk')],
typing.Annotated[FunctionMessageChunk, Tag(tag='FunctionMessageChunk')],
typing.Annotated[ToolMessageChunk, Tag(tag='ToolMessageChunk')]
],
FieldInfo(annotation=NoneType, required=True, discriminator=Discriminator(...))
)
]
},
partial_variables={'system_message': '...', 'name': '...'},
messages=[
SystemMessagePromptTemplate(
prompt=PromptTemplate(template="你的名字是正方辩手,你是一个AI辩论专家,现在正在参加一个两个AI的辩论。...")
),
MessagesPlaceholder(variable_name='messages')
]
)
关键点:
-
input_variables: 定义了模板所需的输入变量,这里是messages。 -
input_types: 指定了messages的类型,包含多种可能的消息类型。 -
partial_variables: 部分填充了system_message和name,允许动态修改系统消息。 -
messages:
SystemMessagePromptTemplate: 定义系统消息,设置AI代理的角色和辩论规则。MessagesPlaceholder: 一个占位符,用于在运行时插入辩论历史记录。
总结:ChatPromptTemplate定义了与AI模型交互的消息格式和内容结构,确保系统消息和历史记录被正确传递给模型。
2. 消息对象
在状态中,messages列表包含不同类型的消息对象,如HumanMessage和AIMessage。
示例:
{
'messages': [
HumanMessage(
content='请开始辩论',
additional_kwargs={},
response_metadata={}
),
AIMessage(
content='非常感谢主持人,各位观众朋友们,大家好!...',
additional_kwargs={},
response_metadata={
'model_name': 'qwen-plus',
'finish_reason': 'stop',
'request_id': '...',
'token_usage': {
'input_tokens': 135,
'output_tokens': 265,
'total_tokens': 400
}
},
name='正方辩手',
id='run-...'
)
]
}
字段说明:
content: 消息的主要内容。additional_kwargs: 额外的关键字参数,当前为空。response_metadata: 包含关于响应的元数据,如使用的模型名称、完成原因、请求ID和令牌使用情况。name(仅AIMessage): 代理的名称(如正方辩手或反方辩手)。id(仅AIMessage): 消息的唯一标识符,用于追踪和调试。
总结:消息对象结构化地存储了对话内容和相关元数据,确保每条消息都包含必要的信息以供系统处理和调试。
3. router 函数逻辑
代码片段:
def router(state):
print(f"router state \n {state}")
messages = state["messages"]
last_message = messages[-1]
if "我辩论失败" in last_message:
return END
return "continue"
分析:
-
目的:根据最新的消息决定辩论是否继续或结束。
-
逻辑:
- 获取
messages列表中的最后一条消息。 - 检查消息内容是否包含“我辩论失败”。
- 如果包含,返回
END,结束辩论。 - 否则,返回
continue,继续辩论。
- 获取
注意事项:
- 类型检查:
last_message是一个AIMessage对象,直接在其上使用in操作符可能不符合预期。正确的做法应该是检查last_message.content是否包含关键字。
建议修改:
def router(state):
print(f"router state \n {state}")
messages = state["messages"]
last_message = messages[-1]
if isinstance(last_message, AIMessage) and "我辩论失败" in last_message.content:
return END
return "continue"
总结:确保router函数正确访问消息内容,以准确判断辩论的终止条件。
4. 状态图(StateGraph)配置
代码片段:
work_flow = StateGraph(State)
work_flow.add_node("node1", node1)
work_flow.add_node("node2", node2)
work_flow.add_edge(START, "node1")
work_flow.add_conditional_edges("node1", router, {"continue":"node2", END:END})
work_flow.add_conditional_edges("node2", router, {"continue":"node1", END:END})
graph = work_flow.compile()
分析:
-
StateGraph(State): 初始化状态图,定义状态的类型。 -
add_node: 添加两个节点,node1(正方辩手)和node2(反方辩手)。 -
add_edge: 定义从START到node1的初始边。 -
add_conditional_edges:-
从node1根据router函数的返回值决定下一步:
- 如果返回
"continue",转到node2。 - 如果返回
END,结束流程。
- 如果返回
-
从node2根据router函数的返回值决定下一步:
- 如果返回
"continue",转到node1。 - 如果返回
END,结束流程。
- 如果返回
-
总结:状态图定义了辩论的流程和逻辑,通过条件边根据代理的回应决定辩论的进展。
5. 辩论流程启动和事件处理
代码片段:
events = graph.stream(
{
"messages": [
HumanMessage(
content="请开始辩论"
)
],
},
{"recursion_limit": 150},
)
for s in events:
print(s)
print("----")
分析:
graph.stream:- 初始状态: 包含一个人类消息“请开始辩论”。
- 配置: 设置递归限制为150步,防止无限循环。
- 事件循环:
for s in events: 遍历辩论过程中的每个事件状态。print(s): 打印当前节点的状态,包括最新的消息。print("----"): 分隔不同的事件状态,便于阅读。
总结:这部分代码负责启动辩论流程,并通过事件循环不断打印辩论状态,帮助您跟踪整个辩论的进展。
总结与建议
1. 数据结构的正确性
-
State类型定义:-
当前代码中
State使用了Annotated[Sequence[BaseMessage], operator.add],这在此上下文中是不正确的。 -
建议:将其修改为
List[BaseMessage],以确保消息列表的正确性。from typing import List class State(TypedDict): messages: List[BaseMessage]
-
2. router 函数的改进
-
问题:当前
router函数直接在last_message上使用in操作符,可能导致类型错误。 -
解决方案:检查
last_message的类型,并在其内容上进行操作。def router(state: Dict[str, Any]) -> Literal["continue", END]: print(f"router state \n {state}") messages = state["messages"] last_message = messages[-1] if isinstance(last_message, AIMessage) and "我辩论失败" in last_message.content: return END return "continue"
3. 消息对象的结构化管理
- AIMessage:
- 确保每个
AIMessage对象包含必要的字段,如content、name和id。 - 避免在
AIMessage中包含未定义的字段(如name),除非AIMessage类明确支持。
- 确保每个
- HumanMessage:
- 确保人类消息包含
content,以及可选的additional_kwargs和response_metadata。
- 确保人类消息包含
4. 打印输出的可读性
-
格式化输出:考虑使用JSON格式或其他结构化格式打印状态,以提高可读性和可解析性。
import json for s in events: print(json.dumps(s, ensure_ascii=False, indent=4)) print("----") -
日志记录:使用日志记录库(如
logging)替代简单的print语句,以便更好地管理日志级别和输出格式。
5. 调试和验证
-
变量打印:在关键步骤打印变量的详细信息,以验证数据的正确性和预期行为。
-
异常处理:在
agent_node和其他关键函数中添加异常处理,以捕获和处理潜在的错误。def agent_node(state, agent, name): try: print(f"agent_node agent \n {agent}") print(f"agent_node state \n {state}") result = agent.invoke(state) result = AIMessage(**result.dict(exclude={"type","name"}), name=name) return { "messages": state["messages"] + [result] } except Exception as e: print(f"Error in agent_node: {e}") return {"messages": state["messages"]}
6. 性能优化
- 递归限制:合理设置递归限制,避免过高或过低。150步可能过高,视实际辩论轮次需求调整。
- 状态管理:确保消息列表的管理是高效的,避免不必要的重复或遗漏。
示例输出结构解析
以下是一个典型的打印输出示例及其结构解析:
agent_node agent
first=ChatPromptTemplate(input_variables=['messages'], input_types={'messages': list[typing.Annotated[typing.Union[...]]]}, partial_variables={'system_message': '...', 'name': '正方辩手'}, messages=[SystemMessagePromptTemplate(prompt=PromptTemplate(...)), MessagesPlaceholder(variable_name='messages')]) middle=[] last=ChatTongyi(client=<class 'dashscope.aigc.generation.Generation'>, model_name='qwen-plus', model_kwargs={}, dashscope_api_key=SecretStr('**********'))
agent_node state
{'messages': [HumanMessage(content='请开始辩论', additional_kwargs={}, response_metadata={})]}
router state
{'messages': [HumanMessage(content='请开始辩论', additional_kwargs={}, response_metadata={}), AIMessage(content='非常感谢主持人,...', additional_kwargs={}, response_metadata={'model_name': 'qwen-plus', 'finish_reason': 'stop', 'request_id': '...', 'token_usage': {'input_tokens': 135, 'output_tokens': 265, 'total_tokens': 400}}, name='正方辩手', id='run-...')]}
{'node1': {'messages': [AIMessage(content='非常感谢主持人,...', additional_kwargs={}, response_metadata={'model_name': 'qwen-plus', 'finish_reason': 'stop', 'request_id': '...', 'token_usage': {'input_tokens': 135, 'output_tokens': 265, 'total_tokens': 400}}, name='正方辩手', id='run-...')]}}
----
结构解析:
agent_node agent部分:first:ChatPromptTemplate对象,定义了输入变量、输入类型、部分变量和消息模板。middle: 中间部分,当前为空。last:ChatTongyi对象,包含模型名称和API密钥等信息。
agent_node state部分:- 当前状态的消息列表,仅包含人类发起的“请开始辩论”消息。
router state部分:- 包含人类消息和AI代理的回应。
AIMessage包含内容、元数据、代理名称和唯一ID。
- 节点状态字典:
node1: 包含AI代理的回应消息列表。----: 分隔符,表示一个事件的结束。
总结:打印输出展示了代理的配置、当前状态的消息列表、以及节点状态的更新。通过这些信息,您可以清晰地跟踪辩论的每个步骤和代理的行为。
进一步建议
-
使用结构化日志:
- 使用JSON或其他结构化格式记录日志,便于后续分析和处理。
import json for s in events: print(json.dumps(s, ensure_ascii=False, indent=4)) print("----") -
增强异常处理:
- 在关键函数中添加异常处理,确保系统在遇到错误时能够优雅地处理并记录错误信息。
def agent_node(state, agent, name): try: print(f"agent_node agent \n {agent}") print(f"agent_node state \n {state}") result = agent.invoke(state) result = AIMessage(**result.dict(exclude={"type","name"}), name=name) return { "messages": state["messages"] + [result] } except Exception as e: print(f"Error in agent_node: {e}") return {"messages": state["messages"]} -
优化
router逻辑:- 确保
router函数正确检查消息内容,避免类型错误。
def router(state: Dict[str, Any]) -> Literal["continue", END]: print(f"router state \n {state}") messages = state["messages"] last_message = messages[-1] if isinstance(last_message, AIMessage) and "我辩论失败" in last_message.content: return END return "continue" - 确保
-
验证消息类型:
- 确保消息对象的类型和内容符合预期,避免因类型不匹配导致的错误。
-
调试和日志记录:
- 在关键步骤添加更多的调试信息和日志记录,以便更好地跟踪系统行为和识别问题。
-
性能监控:
- 监控辩论过程中的性能,如响应时间和资源使用,确保系统在高负载下依然稳定运行。
result
result = AIMessage(**result.dict(exclude={"type","name"}),name=name)
当然,我可以详细解释agent_node函数中的这一行代码:
result = AIMessage(**result.dict(exclude={"type","name"}), name=name)
为了全面理解这一行代码,我们需要分步骤分析其构成和作用。以下是详细的解释:
背景信息
首先,让我们回顾一下agent_node函数的上下文:
def agent_node(state, agent, name):
print(f"agent_node agent \n {agent}")
print(f"agent_node state \n {state}")
result = agent.invoke(state)
result = AIMessage(**result.dict(exclude={"type","name"}), name=name)
return {
"messages": [result]
}
参数说明
state: 当前的对话状态,包含了所有之前的消息。agent: 当前代理(AI辩手),负责生成回应。name: 代理的名称(如“正方辩手”或“反方辩手”)。
函数流程
-
打印代理和状态信息:
print(f"agent_node agent \n {agent}") print(f"agent_node state \n {state}")这些打印语句用于调试,显示当前代理的配置和对话状态。
-
调用代理生成回应:
result = agent.invoke(state)这里,
agent.invoke(state)调用代理生成基于当前对话状态的回应,并将其结果赋值给result。 -
创建
AIMessage对象:result = AIMessage(**result.dict(exclude={"type","name"}), name=name)这行代码将生成的回应转换为一个
AIMessage对象,并附加代理的名称。 -
返回新的消息状态:
return { "messages": [result] }
详细解析
现在,让我们逐步解析这一行代码:
1. result.dict(exclude={"type","name"})
-
result的类型:- 假设
result是一个Pydantic模型实例(如AIMessage的子类),因为它具有.dict()方法。这通常用于将模型实例转换为字典。
- 假设
-
.dict()方法:-
这是Pydantic模型的一个方法,用于将模型实例转换为Python字典。
-
exclude={"type","name"}参数:
-
这个参数指示
.dict()方法在生成字典时排除type和name字段。 -
原因:
- 可能是因为
type字段不需要传递给AIMessage的构造函数,或者它不适用于新的AIMessage实例。 name字段被排除,因为它将在下一步单独传递。
- 可能是因为
-
-
-
示例:
# 假设 result.dict(exclude={"type","name"}) 返回: { "content": "这是AI生成的回应。", "additional_kwargs": {}, "response_metadata": {...}, "id": "run-08504151-1066-4f2c-84e1-16c4ccf50c4f-0" }
2. **result.dict(exclude={"type","name"})
-
**操作符:- 这是Python中的关键字参数展开操作符。它将字典中的键值对作为关键字参数传递给函数或类的构造函数。
-
作用:
- 将
result.dict(exclude={"type","name"})返回的字典中的键值对作为关键字参数传递给AIMessage构造函数。
- 将
-
示例:
AIMessage(content="这是AI生成的回应。", additional_kwargs={}, response_metadata={...}, id="run-08504151-1066-4f2c-84e1-16c4ccf50c4f-0")
3. AIMessage(**result.dict(exclude={"type","name"}), name=name)
-
构造函数调用:
- 这里,
AIMessage的构造函数接收展开的关键字参数和一个额外的name参数。
- 这里,
-
参数说明:
-
**result.dict(exclude={"type","name"}):- 提供了
AIMessage实例所需的其他字段,如content、additional_kwargs、response_metadata和id。
- 提供了
-
name=name:- 将代理的名称(如“正方辩手”或“反方辩手”)作为一个单独的关键字参数传递给
AIMessage构造函数。
- 将代理的名称(如“正方辩手”或“反方辩手”)作为一个单独的关键字参数传递给
-
-
结果:
- 创建了一个新的
AIMessage对象,该对象包含了生成的回应内容、相关的元数据以及代理的名称。
- 创建了一个新的
-
示例:
AIMessage( content="这是AI生成的回应。", additional_kwargs={}, response_metadata={'model_name': 'qwen-plus', 'finish_reason': 'stop', ...}, id="run-08504151-1066-4f2c-84e1-16c4ccf50c4f-0", name="正方辩手" )
4. 为什么这样做?
-
数据封装与扩展:
- 通过将生成的回应转换为
AIMessage对象,并附加代理的名称,系统能够更好地管理和区分不同代理的消息。
- 通过将生成的回应转换为
-
避免重复字段:
- 排除
type字段可能是为了避免在新对象中重复或冲突的字段。
- 排除
-
统一消息格式:
- 确保所有AI生成的消息都有一致的结构,包括内容、元数据、代理名称等,便于后续处理和展示。
5. AIMessage类的假设
-
属性:
content: 消息内容。additional_kwargs: 额外的关键字参数,当前为空。response_metadata: 响应的元数据,如模型名称、完成原因、请求ID和令牌使用情况。id: 消息的唯一标识符。name: 代理的名称。
-
构造函数示例:
class AIMessage(BaseModel): content: str additional_kwargs: Dict[str, Any] = {} response_metadata: Dict[str, Any] = {} id: str name: str
6. 整体流程总结
- 调用代理:
agent.invoke(state)生成一个回应,假设返回一个包含回应内容和元数据的Pydantic模型实例。
- 转换为
AIMessage对象:- 使用
result.dict(exclude={"type","name"})获取回应的字典表示,排除不需要的字段。 - 使用
**操作符展开字典,将其作为关键字参数传递给AIMessage构造函数,同时添加name参数。
- 使用
- 返回新消息:
- 将新创建的
AIMessage对象包装在messages列表中,返回给调用者以更新对话状态。
- 将新创建的
示例
为了更直观地理解,让我们通过一个具体示例展示这一行代码的作用。
假设的result对象
# 假设agent.invoke(state)返回一个Pydantic模型实例,类似于:
result = SomePydanticModel(
type="AIMessage",
name="正方辩手",
content="这是AI生成的回应。",
additional_kwargs={},
response_metadata={
"model_name": "qwen-plus",
"finish_reason": "stop",
"request_id": "run-08504151-1066-4f2c-84e1-16c4ccf50c4f-0",
"token_usage": {
"input_tokens": 135,
"output_tokens": 265,
"total_tokens": 400
}
},
id="run-08504151-1066-4f2c-84e1-16c4ccf50c4f-0"
)
执行代码行
result = AIMessage(**result.dict(exclude={"type","name"}), name=name)
解析步骤
-
result.dict(exclude={"type","name"}):-
生成如下字典:
{ "content": "这是AI生成的回应。", "additional_kwargs": {}, "response_metadata": { "model_name": "qwen-plus", "finish_reason": "stop", "request_id": "run-08504151-1066-4f2c-84e1-16c4ccf50c4f-0", "token_usage": { "input_tokens": 135, "output_tokens": 265, "total_tokens": 400 } }, "id": "run-08504151-1066-4f2c-84e1-16c4ccf50c4f-0" }
-
-
\**result.dict(exclude={"type","name"}):-
将上述字典展开为关键字参数:
content="这是AI生成的回应。", additional_kwargs={}, response_metadata={ "model_name": "qwen-plus", "finish_reason": "stop", "request_id": "run-08504151-1066-4f2c-84e1-16c4ccf50c4f-0", "token_usage": { "input_tokens": 135, "output_tokens": 265, "total_tokens": 400 } }, id="run-08504151-1066-4f2c-84e1-16c4ccf50c4f-0"
-
-
AIMessage(..., name=name):-
使用展开的关键字参数和
name参数创建一个新的
AIMessage对象:
AIMessage( content="这是AI生成的回应。", additional_kwargs={}, response_metadata={ "model_name": "qwen-plus", "finish_reason": "stop", "request_id": "run-08504151-1066-4f2c-84e1-16c4ccf50c4f-0", "token_usage": { "input_tokens": 135, "output_tokens": 265, "total_tokens": 400 } }, id="run-08504151-1066-4f2c-84e1-16c4ccf50c4f-0", name="正方辩手" )
-
最终结果
-
result: 一个新的AIMessage对象,包含了生成的回应内容、元数据、唯一ID以及代理的名称。 -
返回值:
return { "messages": [result] }这将更新对话状态,包含新的AI回应消息。
为什么这么做?
1. 数据转换和标准化
- 统一消息格式:
- 通过将生成的回应转换为
AIMessage对象,可以确保所有AI生成的消息具有一致的结构和属性。这有助于后续处理和展示消息。
- 通过将生成的回应转换为
- 类型安全:
- 使用Pydantic模型(假设
AIMessage是一个Pydantic模型)可以确保消息数据的类型和格式正确,减少潜在的错误。
- 使用Pydantic模型(假设
2. 动态属性设置
- 代理名称
- 通过传递
name=name,您可以在每条AI消息中标明是哪个代理(正方或反方)生成了该消息。这对于跟踪和区分不同代理的发言非常有用。
- 通过传递
3. 排除不必要的字段
exclude={"type","name"}:- 可能是因为
type字段在新对象中不需要,或者在构造新的AIMessage对象时会自动推断。 - 排除
name字段是因为它将被单独传递,而不是通过展开的字典传递。
- 可能是因为
4. 简化状态更新
-
状态管理:
- 将生成的
AIMessage对象直接插入到messages列表中,简化了对话状态的管理和更新。
- 将生成的
-
减少冗余:
- 通过排除某些字段,可以避免在消息对象中出现冗余或冲突的数据。
假设与依赖
为了完全理解这行代码,我们需要了解一些假设和依赖:
1. AIMessage类
-
定义:
from pydantic import BaseModel from typing import Any, Dict class AIMessage(BaseModel): content: str additional_kwargs: Dict[str, Any] = {} response_metadata: Dict[str, Any] = {} id: str name: str- 假设
AIMessage是一个Pydantic模型,包含上述字段。
- 假设
2. result对象
-
类型:
- 假设
result是一个Pydantic模型实例,具有.dict()方法,能够生成包含必要字段的字典表示。
- 假设
3. 代理的返回结构
agent.invoke(state)的返回值:- 假设它返回一个包含生成的回应内容和元数据的Pydantic模型实例。
完整示例
为了进一步说明,让我们构建一个完整的示例,展示如何从代理调用到最终的AIMessage对象的生成。
定义相关类
from pydantic import BaseModel
from typing import Any, Dict
class AIMessage(BaseModel):
content: str
additional_kwargs: Dict[str, Any] = {}
response_metadata: Dict[str, Any] = {}
id: str
name: str
class SomePydanticModel(BaseModel):
type: str
name: str
content: str
additional_kwargs: Dict[str, Any]
response_metadata: Dict[str, Any]
id: str
模拟代理的invoke方法
def mock_invoke(state):
# 模拟生成AI回应
return SomePydanticModel(
type="AIMessage",
name="正方辩手",
content="这是AI生成的回应。",
additional_kwargs={},
response_metadata={
"model_name": "qwen-plus",
"finish_reason": "stop",
"request_id": "run-08504151-1066-4f2c-84e1-16c4ccf50c4f-0",
"token_usage": {
"input_tokens": 135,
"output_tokens": 265,
"total_tokens": 400
}
},
id="run-08504151-1066-4f2c-84e1-16c4ccf50c4f-0"
)
完整的agent_node函数
def agent_node(state, agent, name):
print(f"agent_node agent \n {agent}")
print(f"agent_node state \n {state}")
result = agent.invoke(state) # 调用代理生成回应
# 将生成的回应转换为AIMessage对象,排除'type'和'name'字段,并传递'name'参数
result = AIMessage(**result.dict(exclude={"type","name"}), name=name)
return {
"messages": [result]
}
执行示例
# 初始化状态
state = {
"messages": [
{"content": "请开始辩论", "additional_kwargs": {}, "response_metadata": {}}
]
}
# 创建一个模拟代理
class MockAgent:
def invoke(self, state):
return mock_invoke(state)
agent = MockAgent()
# 调用agent_node函数
new_state = agent_node(state, agent, "正方辩手")
# 打印新状态
print(new_state)
输出解释
执行上述代码,将得到如下输出:
agent_node agent
<__main__.ChatPromptTemplate object at 0x7f8d3c4e5a60>
agent_node state
{'messages': [{'content': '请开始辩论', 'additional_kwargs': {}, 'response_metadata': {}}]}
{'messages': [AIMessage(content='这是AI生成的回应.', additional_kwargs={}, response_metadata={'model_name': 'qwen-plus', 'finish_reason': 'stop', 'request_id': 'run-08504151-1066-4f2c-84e1-16c4ccf50c4f-0', 'token_usage': {'input_tokens': 135, 'output_tokens': 265, 'total_tokens': 400}}, id='run-08504151-1066-4f2c-84e1-16c4ccf50c4f-0', name='正方辩手')]}
逐步解释
-
打印代理信息:
agent_node agent <__main__.ChatPromptTemplate object at 0x7f8d3c4e5a60>- 显示代理对象的内存地址和类型。
-
打印当前状态:
agent_node state {'messages': [{'content': '请开始辩论', 'additional_kwargs': {}, 'response_metadata': {}}]}- 显示当前的对话状态,仅包含人类消息“请开始辩论”。
-
调用
agent.invoke(state):- 生成一个新的
SomePydanticModel实例,包含AI回应的详细信息。
- 生成一个新的
-
转换为
AIMessage对象:- 使用
result.dict(exclude={"type","name"})获取字典表示,排除type和name字段。 - 使用
**展开字典,将其作为关键字参数传递给AIMessage构造函数,同时传递name="正方辩手"。
- 使用
-
打印和返回新状态:
{'messages': [AIMessage(content='这是AI生成的回应.', additional_kwargs={}, response_metadata={'model_name': 'qwen-plus', 'finish_reason': 'stop', 'request_id': 'run-08504151-1066-4f2c-84e1-16c4ccf50c4f-0', 'token_usage': {'input_tokens': 135, 'output_tokens': 265, 'total_tokens': 400}}, id='run-08504151-1066-4f2c-84e1-16c4ccf50c4f-0', name='正方辩手')]}- 新的对话状态包含一个
AIMessage对象,包含AI生成的回应内容、相关元数据、唯一ID以及代理名称。
- 新的对话状态包含一个
agent
type 0f agent:<class 'langchain_core.runnables.base.RunnableSequence'>
当然,我很高兴为您详细解释invoke方法的实现。这段代码来自于一个名为langchain_core.beta.runnables的模块,可能是LangChain库的一部分,用于处理可运行对象(Runnables)的执行流程。以下是对该invoke方法的逐行解释,以及其在整体系统中的作用。
方法签名
def invoke(
self, input: Input, config: Optional[RunnableConfig] = None, **kwargs: Any
) -> Output:
参数说明
self: 指向当前实例的引用。input: Input: 输入数据,类型为Input,可能是一个字符串、字典或其他数据结构,具体取决于应用场景。config: Optional[RunnableConfig]: 可选的配置参数,类型为RunnableConfig。用于定制执行环境或行为。\**kwargs: Any: 其他可选的关键字参数,用于传递额外的信息或配置。
返回值
Output: 方法的输出,类型为Output,具体类型取决于实现细节。
详细解析
1. 导入必要的模块
from langchain_core.beta.runnables.context import config_with_context
config_with_context: 从langchain_core.beta.runnables.context模块导入的函数,用于将配置与上下文结合起来,确保执行过程中的上下文一致性。
2. 设置回调和上下文
# setup callbacks and context
config = config_with_context(ensure_config(config), self.steps)
callback_manager = get_callback_manager_for_config(config)
-
ensure_config(config): 确保传入的config参数符合预期的格式或结构。如果config为None,则提供默认配置。 -
config_with_context(ensure_config(config), self.steps):-
作用: 将配置与当前可运行对象的步骤(
self.steps)结合起来,生成一个完整的配置对象,包含执行上下文信息。 -
参数
:
ensure_config(config): 已确认或默认的配置对象。self.steps: 当前可运行对象的执行步骤,通常是一个可迭代的步骤列表或链。
-
-
get_callback_manager_for_config(config):-
作用: 根据配置对象获取相应的回调管理器(
callback_manager),用于处理执行过程中的各种回调(如日志记录、事件处理等)。 -
参数
:
config: 包含执行上下文和回调配置的对象。
-
3. 启动根运行(Root Run)
# start the root run
run_manager = callback_manager.on_chain_start(
None,
input,
name=config.get("run_name") or self.get_name(),
run_id=config.pop("run_id", None),
)
callback_manager.on_chain_start(...):-
作用: 通知回调管理器链(Chain)开始执行。创建一个运行管理器(
run_manager),用于管理当前运行的生命周期。 -
参数:
-
None: 通常是指父运行(Parent Run),这里为None表示这是根运行。 -
input: 当前运行的输入数据。 -
name=config.get("run_name") or self.get_name():- 获取运行的名称。如果配置中指定了
run_name,则使用该名称;否则,使用可运行对象的名称(self.get_name())。
- 获取运行的名称。如果配置中指定了
-
run_id=config.pop("run_id", None):- 获取运行的唯一标识符(
run_id)。如果配置中存在run_id,则使用并从配置中移除;否则,使用None。
- 获取运行的唯一标识符(
-
-
4. 逐步调用所有步骤
# invoke all steps in sequence
try:
for i, step in enumerate(self.steps):
# mark each step as a child run
config = patch_config(
config, callbacks=run_manager.get_child(f"seq:step:{i+1}")
)
context = copy_context()
context.run(_set_config_context, config)
if i == 0:
input = context.run(step.invoke, input, config, **kwargs)
else:
input = context.run(step.invoke, input, config)
# finish the root run
except BaseException as e:
run_manager.on_chain_error(e)
raise
else:
run_manager.on_chain_end(input)
return cast(Output, input)
a. try 块
- 目的: 确保即使在执行过程中出现异常,也能正确处理并通知回调管理器。
b. 循环执行每个步骤
for i, step in enumerate(self.steps):
self.steps: 当前可运行对象的步骤列表,每个步骤可能是一个可调用的对象(如函数、方法或其他可运行对象)。enumerate: 提供步骤的索引i和步骤对象step。
c. 为每个步骤标记子运行(Child Run)
config = patch_config(
config, callbacks=run_manager.get_child(f"seq:step:{i+1}")
)
-
run_manager.get_child(f"seq:step:{i+1}"):- 作用: 获取当前步骤的子运行管理器,用于管理该步骤的生命周期和回调。
- 参数: 生成子运行的名称,例如
"seq:step:1"、"seq:step:2"等。
-
patch_config(config, callbacks=...):-
作用: 更新配置对象,添加子运行的回调配置,以便子运行能够正确处理其回调。
-
参数
:
config: 当前的配置对象。callbacks=...: 子运行的回调配置。
-
d. 复制上下文并设置配置上下文
context = copy_context()
context.run(_set_config_context, config)
-
copy_context():- 作用: 复制当前的上下文(Context),确保每个步骤在独立的上下文中执行,不互相干扰。
- 返回: 一个新的上下文对象。
-
context.run(_set_config_context, config):-
作用: 在复制的上下文中运行
_set_config_context函数,设置当前步骤的配置上下文。 -
参数:
_set_config_context: 一个函数,用于将配置对象设置到上下文中。config: 当前步骤的配置对象。
-
e. 调用步骤的invoke方法
if i == 0:
input = context.run(step.invoke, input, config, **kwargs)
else:
input = context.run(step.invoke, input, config)
-
context.run(...):- 作用: 在当前上下文中运行指定的函数或方法,确保配置和回调正确传递。
-
step.invoke(...):- 作用: 调用步骤的
invoke方法,传递必要的参数,执行该步骤的逻辑。
- 作用: 调用步骤的
-
参数区别:
-
第一个步骤(
i == 0):- 传递
input,config, 以及额外的关键字参数**kwargs。
- 传递
-
后续步骤:
- 仅传递
input和config,不再传递**kwargs。
- 仅传递
-
-
更新
input:- 将步骤的输出作为下一个步骤的输入,实现步骤之间的链式调用。
f. 异常处理
except BaseException as e:
run_manager.on_chain_error(e)
raise
except BaseException as e:- 捕获所有类型的异常(包括
SystemExit、KeyboardInterrupt等),确保即使在遇到严重错误时也能处理。
- 捕获所有类型的异常(包括
run_manager.on_chain_error(e):- 作用: 通知回调管理器链执行过程中发生了错误,触发相应的错误回调。
raise:- 重新引发异常,确保错误能够被上层处理或记录。
g. 成功执行后的处理
else:
run_manager.on_chain_end(input)
return cast(Output, input)
-
run_manager.on_chain_end(input):- 作用: 通知回调管理器链执行结束,传递最终的输出结果。
-
return cast(Output, input):-
作用: 将最终的输入(经过所有步骤处理后的输出)转换为
Output类型并返回。 -
cast:
- 用于类型提示,确保返回值符合预期的
Output类型。
- 用于类型提示,确保返回值符合预期的
-
总体流程概述
- 配置和回调设置:
- 确保配置对象包含必要的上下文信息。
- 获取回调管理器,设置执行过程中的回调机制。
- 启动根运行:
- 通知回调管理器链(Chain)开始执行,创建一个运行管理器来管理整个执行流程。
- 逐步执行步骤:
- 遍历所有步骤,逐步调用每个步骤的
invoke方法。 - 为每个步骤创建独立的子运行和回调配置,确保执行过程的隔离和管理。
- 更新
input,将每个步骤的输出作为下一个步骤的输入,实现链式调用。
- 遍历所有步骤,逐步调用每个步骤的
- 异常处理:
- 如果在任何步骤中出现异常,通知回调管理器链并重新引发异常,确保错误被正确处理和记录。
- 完成运行:
- 如果所有步骤成功执行,通知回调管理器链执行结束,返回最终的输出结果。
关键组件和概念
1. 配置对象(config)
- 作用:存储执行过程中的配置信息,如回调设置、运行名称、运行ID等。
- 管理:通过
config_with_context和patch_config函数动态更新和管理。
2. 回调管理器(callback_manager)
-
作用:管理执行过程中的回调函数,如日志记录、事件处理等。
-
功能
:
on_chain_start:在链开始时触发回调。on_chain_end:在链结束时触发回调。on_chain_error:在链执行过程中发生错误时触发回调。get_child:获取子运行的回调管理器,用于独立管理每个步骤的回调。
3. 运行管理器(run_manager)
-
作用:管理当前运行的生命周期,包括启动、结束和错误处理。
-
方法:
on_chain_start:初始化根运行。on_chain_end:完成根运行。on_chain_error:处理根运行中的错误。get_child:获取子运行的运行管理器,用于步骤级别的管理。
4. 上下文管理(context)
-
作用:确保每个步骤在独立的上下文中执行,避免步骤之间的状态干扰。
-
功能:
copy_context:复制当前的执行上下文。_set_config_context:在上下文中设置当前步骤的配置。
5. 步骤执行(self.steps)
-
作用:包含了所有要执行的步骤,每个步骤负责特定的任务或逻辑。
-
调用方式:
step.invoke(input, config, \**kwargs):调用步骤的invoke方法,传递必要的参数。
具体操作指导
1. 确认invoke方法的类型和来源
根据您的先前问题,您已经通过打印type(result)确认了result的类型,并查看了type和name字段。现在,理解invoke方法的实现可以帮助您更好地调试和扩展系统。
2. 打印和理解result中的字段
如您所做的,通过在agent_node函数中添加打印语句,可以查看result对象的各个字段:
def agent_node(state, agent, name):
print(f"agent_node agent \n {agent}")
print(f"agent_node state \n {state}")
# 打印agent的类型
print(f"Type of agent: {type(agent)}")
# 调用代理生成回应
result = agent.invoke(state)
# 打印result的类型和字段
print(f"Type of result: {type(result)}")
if hasattr(result, 'type'):
print(f"result.type: {result.type}")
else:
print("result 没有 'type' 字段")
if hasattr(result, 'name'):
print(f"result.name: {result.name}")
else:
print("result 没有 'name' 字段")
# 将name添加到content的最前面
if hasattr(result, 'content'):
original_content = result.content
new_content = f"{name}: {original_content}"
print(f"Original content: {original_content}")
print(f"New content: {new_content}")
else:
print("result 没有 'content' 字段")
new_content = result.content # 保持不变
# 创建新的AIMessage对象,排除'type'和'name',并传入新的'name'
result_dict = result.dict(exclude={"type","name"})
result_dict['content'] = new_content # 更新content字段
result = AIMessage(**result_dict, name=name)
return {
"messages": [result]
}
3. 理解为什么要排除type和name字段
type字段:- 目的:通常用于标识消息的类型,但在新的
AIMessage对象中,可能不需要此字段,或者会引起冲突。 - 操作:通过
exclude={"type"}从字典中排除该字段。
- 目的:通常用于标识消息的类型,但在新的
name字段:- 目的:原始的
name字段可能包含旧的代理名称(如“正方辩手”),而您希望使用传入的name参数来更新。 - 操作:通过
exclude={"name"}排除原有的name字段,并在构造新的AIMessage对象时传入新的name。
- 目的:原始的
4. 查看result中原来的name字段的值
通过打印result.name,您可以确认原始代理名称是否符合预期:
result.name: 正方辩手
这表明result对象中原有的name字段值为“正方辩手”,在创建新的AIMessage对象时,您将其替换为传入的name参数。
5. 将name参数添加到content的最前面
为增强消息的可读性和信息量,您可以将代理的名称添加到content字段的最前面:
if hasattr(result, 'content'):
original_content = result.content
new_content = f"{name}: {original_content}"
print(f"Original content: {original_content}")
print(f"New content: {new_content}")
else:
print("result 没有 'content' 字段")
new_content = result.content # 保持不变
-
操作步骤:
- 检查
result对象是否具有content字段。 - 获取原始内容
original_content。 - 创建新的内容字符串
new_content,格式为"{name}: {original_content}"。 - 更新
result_dict['content']为新的内容字符串。
- 检查
6. 创建新的AIMessage对象
将修改后的内容和新的name字段一起构造新的AIMessage对象:
result_dict = result.dict(exclude={"type","name"})
result_dict['content'] = new_content # 更新content字段
result = AIMessage(**result_dict, name=name)
-
操作步骤:
- 使用
result.dict(exclude={"type","name"})获取result对象的字典表示,排除type和name字段。 - 更新
result_dict['content']为新的内容字符串。 - 使用展开的关键字参数
**result_dict和新的name参数,创建一个新的AIMessage对象。
- 使用
7. 返回更新后的消息状态
return {
"messages": [result]
}
- 作用:将新的
AIMessage对象作为消息列表的一部分返回,以更新对话状态。
完整示例
为了更好地理解整个过程,以下是一个完整的示例,包括类定义、模拟代理、以及agent_node函数的实现。
定义相关类
from pydantic import BaseModel
from typing import Any, Dict
class AIMessage(BaseModel):
content: str
additional_kwargs: Dict[str, Any] = {}
response_metadata: Dict[str, Any] = {}
id: str
name: str
class SomePydanticModel(BaseModel):
type: str
name: str
content: str
additional_kwargs: Dict[str, Any]
response_metadata: Dict[str, Any]
id: str
模拟代理的invoke方法
def mock_invoke(state):
# 模拟生成AI回应
return SomePydanticModel(
type="AIMessage",
name="正方辩手",
content="这是AI生成的回应。",
additional_kwargs={},
response_metadata={
"model_name": "qwen-plus",
"finish_reason": "stop",
"request_id": "run-08504151-1066-4f2c-84e1-16c4ccf50c4f-0",
"token_usage": {
"input_tokens": 135,
"output_tokens": 265,
"total_tokens": 400
}
},
id="run-08504151-1066-4f2c-84e1-16c4ccf50c4f-0"
)
创建一个模拟代理类
class MockAgent:
def invoke(self, state):
return mock_invoke(state)
实现agent_node函数
def agent_node(state, agent, name):
print(f"agent_node agent \n {agent}")
print(f"agent_node state \n {state}")
# 打印agent的类型
print(f"Type of agent: {type(agent)}")
# 调用代理生成回应
result = agent.invoke(state)
# 打印result的类型和字段
print(f"Type of result: {type(result)}")
if hasattr(result, 'type'):
print(f"result.type: {result.type}")
else:
print("result 没有 'type' 字段")
if hasattr(result, 'name'):
print(f"result.name: {result.name}")
else:
print("result 没有 'name' 字段")
# 将name添加到content的最前面
if hasattr(result, 'content'):
original_content = result.content
new_content = f"{name}: {original_content}"
print(f"Original content: {original_content}")
print(f"New content: {new_content}")
else:
print("result 没有 'content' 字段")
new_content = result.content # 保持不变
# 创建新的AIMessage对象,排除'type'和'name',并传入新的'name'
result_dict = result.dict(exclude={"type","name"})
result_dict['content'] = new_content # 更新content字段
result = AIMessage(**result_dict, name=name)
return {
"messages": [result]
}
执行示例
# 初始化状态
state = {
"messages": [
{"content": "请开始辩论", "additional_kwargs": {}, "response_metadata": {}}
]
}
# 创建一个模拟代理
agent = MockAgent()
# 调用agent_node函数
new_state = agent_node(state, agent, "正方辩手")
# 打印新状态
print(new_state)
预期输出
agent_node agent
<__main__.MockAgent object at 0x7f8d3c4e5a60>
agent_node state
{'messages': [{'content': '请开始辩论', 'additional_kwargs': {}, 'response_metadata': {}}]}
Type of agent: <class '__main__.MockAgent'>
Type of result: <class '__main__.SomePydanticModel'>
result.type: AIMessage
result.name: 正方辩手
Original content: 这是AI生成的回应。
New content: 正方辩手: 这是AI生成的回应。
{'messages': [AIMessage(content='正方辩手: 这是AI生成的回应。', additional_kwargs={}, response_metadata={'model_name': 'qwen-plus', 'finish_reason': 'stop', 'request_id': 'run-08504151-1066-4f2c-84e1-16c4ccf50c4f-0', 'token_usage': {'input_tokens': 135, 'output_tokens': 265, 'total_tokens': 400}}, id='run-08504151-1066-4f2c-84e1-16c4ccf50c4f-0', name='正方辩手')]}
通过这个完整的示例,您可以清楚地看到:
- 代理信息被打印出来,显示了代理对象的内存地址和类型。
- 当前状态显示了人类发起的“请开始辩论”消息。
result对象的类型被确认为SomePydanticModel。result.type和result.name字段的值被打印出来。content字段被修改为name参数加上原始内容。- 新的
AIMessage对象被创建,并包含更新后的内容和新的name字段。 - 新状态显示了更新后的消息列表。
总结
1. invoke方法的主要功能
- 配置和回调管理:
- 确保每个运行都有适当的配置和回调机制,以处理执行过程中的事件和日志。
- 步骤执行:
- 逐步调用每个步骤的
invoke方法,传递必要的参数,实现链式执行。 - 通过子运行和独立的回调管理,确保每个步骤的执行独立且可监控。
- 逐步调用每个步骤的
- 异常处理:
- 在执行过程中捕获所有异常,确保回调管理器能够正确处理错误,并重新引发异常以供上层处理。
- 结果管理:
- 将每个步骤的输出作为下一个步骤的输入,最终返回整个执行链的输出结果。
2. 为什么在agent_node函数中要排除type和name字段
-
避免字段冲突:
- 原始的
type和name字段可能与新的AIMessage对象的字段冲突,因此需要排除。
- 原始的
-
动态更新
name字段:- 通过传入新的
name参数,确保每条AI消息都标明生成该消息的代理(如“正方辩手”或“反方辩手”)。
- 通过传入新的
3. 将name参数添加到content字段的最前面
- 增强消息的可读性:
- 通过在消息内容前添加代理名称,可以更清晰地标识出是哪一方生成的回应。
- 信息量提升:
- 让消息内容更加丰富,包含代理身份信息,便于后续处理和展示。
4. 建议与最佳实践
- 结构化日志记录:
- 使用结构化日志记录工具(如
logging模块)代替简单的print语句,以便更好地管理和分析日志。
- 使用结构化日志记录工具(如
- 类型安全与验证:
- 使用Pydantic等工具确保数据的类型和结构正确,减少运行时错误。
- 异常处理:
- 在关键步骤添加更多的异常处理逻辑,确保系统的健壮性和可维护性。
- 代码可读性与维护:
- 添加详细的注释和文档,帮助团队成员理解代码逻辑和意图。

768

被折叠的 条评论
为什么被折叠?



