multi agents辩论

代码

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_types
      定义了
      messages
      

      的类型为一个注释(

      Annotated
      

      )的列表,列表中的元素可以是多种消息类型的联合(

      Union
      

      ),包括:

      • AIMessage
      • HumanMessage
      • ChatMessage
      • SystemMessage
      • FunctionMessage
      • ToolMessage
      • 以及它们的分块版本(如AIMessageChunk等)
    • partial_variables: 部分变量填充了system_messagename,用于动态生成系统提示。

    • messages
      包含两个部分:
      • SystemMessagePromptTemplate: 系统消息模板,定义了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={})]}

分析

  • state
    当前的状态是一个字典,包含一个键
    messages
    

    ,其值是一个消息列表。

    • HumanMessage
      表示人类发起的消息,内容为“请开始辩论”。
      • content: 消息内容。
      • 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='...')]}

分析

  • state
    类似于
    agent_node
    

    函数中的状态字典,但此时包含了人类消息和AI代理的回应。

    • AIMessage
      表示AI代理(正方辩手)的回应。
      • content: AI的回应内容(具体内容在您的日志中被省略)。

      • additional_kwargs: 额外的关键字参数,当前为空。

      • response_metadata
        响应的元数据,包括:
        • model_name: 使用的模型名称。
        • finish_reason: 响应完成的原因,如stop
        • request_id: 请求ID,用于追踪。
        • token_usage: 令牌使用情况,包含input_tokensoutput_tokenstotal_tokens
      • name: AI代理的名称(正方辩手)。

      • id: 消息的唯一标识符。

总结:这部分打印输出展示了AI代理的回应,包含了丰富的元数据信息,有助于调试和追踪请求。

3. 更新后的节点状态

agent_noderouter函数之后,代码打印了更新后的节点状态:

{'node1': {'messages': [AIMessage(content='非常感谢主持人,各位观众朋友们,大家好!...', ...)]}}
----

分析

  • node1
    表示正方辩手的节点。
    • messages: 当前节点的消息列表,包含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代理的回应。
  • 节点更新:每轮结束后,节点(node1node2)的消息列表被更新,并通过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_messagename,允许动态修改系统消息。

  • messages

    :

    • SystemMessagePromptTemplate: 定义系统消息,设置AI代理的角色和辩论规则。
    • MessagesPlaceholder: 一个占位符,用于在运行时插入辩论历史记录。

总结ChatPromptTemplate定义了与AI模型交互的消息格式和内容结构,确保系统消息和历史记录被正确传递给模型。

2. 消息对象

在状态中,messages列表包含不同类型的消息对象,如HumanMessageAIMessage

示例

{
    '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: 定义从STARTnode1的初始边。

  • 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对象包含必要的字段,如contentnameid
    • 避免在AIMessage中包含未定义的字段(如name),除非AIMessage类明确支持。
  • HumanMessage:
    • 确保人类消息包含content,以及可选的additional_kwargsresponse_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-...')]}}
----

结构解析

  1. agent_node agent 部分
    • first: ChatPromptTemplate对象,定义了输入变量、输入类型、部分变量和消息模板。
    • middle: 中间部分,当前为空。
    • last: ChatTongyi对象,包含模型名称和API密钥等信息。
  2. agent_node state 部分
    • 当前状态的消息列表,仅包含人类发起的“请开始辩论”消息。
  3. router state 部分
    • 包含人类消息和AI代理的回应。
    • AIMessage 包含内容、元数据、代理名称和唯一ID。
  4. 节点状态字典
    • node1: 包含AI代理的回应消息列表。
    • ----: 分隔符,表示一个事件的结束。

总结:打印输出展示了代理的配置、当前状态的消息列表、以及节点状态的更新。通过这些信息,您可以清晰地跟踪辩论的每个步骤和代理的行为。

进一步建议

  1. 使用结构化日志

    • 使用JSON或其他结构化格式记录日志,便于后续分析和处理。
    import json
    
    for s in events:
        print(json.dumps(s, ensure_ascii=False, indent=4))
        print("----")
    
  2. 增强异常处理

    • 在关键函数中添加异常处理,确保系统在遇到错误时能够优雅地处理并记录错误信息。
    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"]}
    
  3. 优化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"
    
  4. 验证消息类型

    • 确保消息对象的类型和内容符合预期,避免因类型不匹配导致的错误。
  5. 调试和日志记录

    • 在关键步骤添加更多的调试信息和日志记录,以便更好地跟踪系统行为和识别问题。
  6. 性能监控

    • 监控辩论过程中的性能,如响应时间和资源使用,确保系统在高负载下依然稳定运行。

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: 代理的名称(如“正方辩手”或“反方辩手”)。

函数流程

  1. 打印代理和状态信息

    print(f"agent_node agent \n {agent}")
    print(f"agent_node state \n {state}")
    

    这些打印语句用于调试,显示当前代理的配置和对话状态。

  2. 调用代理生成回应

    result = agent.invoke(state)
    

    这里,agent.invoke(state)调用代理生成基于当前对话状态的回应,并将其结果赋值给result

  3. 创建AIMessage对象

    result = AIMessage(**result.dict(exclude={"type","name"}), name=name)
    

    这行代码将生成的回应转换为一个AIMessage对象,并附加代理的名称。

  4. 返回新的消息状态

    return {
        "messages": [result]
    }
    

详细解析

现在,让我们逐步解析这一行代码:

1. result.dict(exclude={"type","name"})

  • result的类型

    • 假设result是一个Pydantic模型实例(如AIMessage的子类),因为它具有.dict()方法。这通常用于将模型实例转换为字典。
  • .dict()方法

    • 这是Pydantic模型的一个方法,用于将模型实例转换为Python字典。

    • exclude={"type","name"}

      参数:

      • 这个参数指示.dict()方法在生成字典时排除typename字段。

      • 原因:

        • 可能是因为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实例所需的其他字段,如contentadditional_kwargsresponse_metadataid
    • 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. 整体流程总结

  1. 调用代理
    • agent.invoke(state)生成一个回应,假设返回一个包含回应内容和元数据的Pydantic模型实例。
  2. 转换为AIMessage对象
    • 使用result.dict(exclude={"type","name"})获取回应的字典表示,排除不需要的字段。
    • 使用**操作符展开字典,将其作为关键字参数传递给AIMessage构造函数,同时添加name参数。
  3. 返回新消息
    • 将新创建的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)

解析步骤

  1. 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"
      }
      
  2. \**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"
      
  3. 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模型)可以确保消息数据的类型和格式正确,减少潜在的错误。

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='正方辩手')]}

逐步解释

  1. 打印代理信息

    agent_node agent 
     <__main__.ChatPromptTemplate object at 0x7f8d3c4e5a60>
    
    • 显示代理对象的内存地址和类型。
  2. 打印当前状态

    agent_node state 
     {'messages': [{'content': '请开始辩论', 'additional_kwargs': {}, 'response_metadata': {}}]}
    
    • 显示当前的对话状态,仅包含人类消息“请开始辩论”。
  3. 调用agent.invoke(state)

    • 生成一个新的SomePydanticModel实例,包含AI回应的详细信息。
  4. 转换为AIMessage对象

    • 使用result.dict(exclude={"type","name"})获取字典表示,排除typename字段。
    • 使用**展开字典,将其作为关键字参数传递给AIMessage构造函数,同时传递name="正方辩手"
  5. 打印和返回新状态

    {'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参数符合预期的格式或结构。如果configNone,则提供默认配置。

  • 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
    • 后续步骤:

      • 仅传递inputconfig,不再传递**kwargs
  • 更新input:

    • 将步骤的输出作为下一个步骤的输入,实现步骤之间的链式调用。
f. 异常处理
except BaseException as e:
    run_manager.on_chain_error(e)
    raise
  • except BaseException as e:
    • 捕获所有类型的异常(包括SystemExitKeyboardInterrupt等),确保即使在遇到严重错误时也能处理。
  • 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类型。

总体流程概述

  1. 配置和回调设置
    • 确保配置对象包含必要的上下文信息。
    • 获取回调管理器,设置执行过程中的回调机制。
  2. 启动根运行
    • 通知回调管理器链(Chain)开始执行,创建一个运行管理器来管理整个执行流程。
  3. 逐步执行步骤
    • 遍历所有步骤,逐步调用每个步骤的invoke方法。
    • 为每个步骤创建独立的子运行和回调配置,确保执行过程的隔离和管理。
    • 更新input,将每个步骤的输出作为下一个步骤的输入,实现链式调用。
  4. 异常处理
    • 如果在任何步骤中出现异常,通知回调管理器链并重新引发异常,确保错误被正确处理和记录。
  5. 完成运行
    • 如果所有步骤成功执行,通知回调管理器链执行结束,返回最终的输出结果。

关键组件和概念

1. 配置对象(config

  • 作用:存储执行过程中的配置信息,如回调设置、运行名称、运行ID等。
  • 管理:通过config_with_contextpatch_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的类型,并查看了typename字段。现在,理解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. 理解为什么要排除typename字段

  • 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  # 保持不变
  • 操作步骤:

    1. 检查result对象是否具有content字段。
    2. 获取原始内容original_content
    3. 创建新的内容字符串new_content,格式为"{name}: {original_content}"
    4. 更新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)
  • 操作步骤:

    1. 使用result.dict(exclude={"type","name"})获取result对象的字典表示,排除typename字段。
    2. 更新result_dict['content']为新的内容字符串。
    3. 使用展开的关键字参数**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='正方辩手')]}

通过这个完整的示例,您可以清楚地看到:

  1. 代理信息被打印出来,显示了代理对象的内存地址和类型。
  2. 当前状态显示了人类发起的“请开始辩论”消息。
  3. result对象的类型被确认为SomePydanticModel
  4. result.typeresult.name字段的值被打印出来。
  5. content字段被修改为name参数加上原始内容。
  6. 新的AIMessage对象被创建,并包含更新后的内容和新的name字段。
  7. 新状态显示了更新后的消息列表。

总结

1. invoke方法的主要功能

  • 配置和回调管理
    • 确保每个运行都有适当的配置和回调机制,以处理执行过程中的事件和日志。
  • 步骤执行
    • 逐步调用每个步骤的invoke方法,传递必要的参数,实现链式执行。
    • 通过子运行和独立的回调管理,确保每个步骤的执行独立且可监控。
  • 异常处理
    • 在执行过程中捕获所有异常,确保回调管理器能够正确处理错误,并重新引发异常以供上层处理。
  • 结果管理
    • 将每个步骤的输出作为下一个步骤的输入,最终返回整个执行链的输出结果。

2. 为什么在agent_node函数中要排除typename字段

  • 避免字段冲突:

    • 原始的typename字段可能与新的AIMessage对象的字段冲突,因此需要排除。
  • 动态更新name字段:

    • 通过传入新的name参数,确保每条AI消息都标明生成该消息的代理(如“正方辩手”或“反方辩手”)。

3. 将name参数添加到content字段的最前面

  • 增强消息的可读性
    • 通过在消息内容前添加代理名称,可以更清晰地标识出是哪一方生成的回应。
  • 信息量提升
    • 让消息内容更加丰富,包含代理身份信息,便于后续处理和展示。

4. 建议与最佳实践

  • 结构化日志记录
    • 使用结构化日志记录工具(如logging模块)代替简单的print语句,以便更好地管理和分析日志。
  • 类型安全与验证
    • 使用Pydantic等工具确保数据的类型和结构正确,减少运行时错误。
  • 异常处理
    • 在关键步骤添加更多的异常处理逻辑,确保系统的健壮性和可维护性。
  • 代码可读性与维护
    • 添加详细的注释和文档,帮助团队成员理解代码逻辑和意图。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值