AutoGen 技术博客系列 (四):自定义智能体的高级技巧

88 阅读19分钟

AutoGen系列一:基础介绍与入门教程

AutoGen系列二:深入自定义智能体

AutoGen系列三:内置智能体的应用与实战

AutoGen系列四: 自定义智能体的高级技巧

AutoGen系列五: 智能体团队协作的深度剖析与实践

AutoGen系列六: SelectorGroupChat 的原理与实践

AutoGen 技术博客系列 (七):状态管理与组件序列化解析

AutoGen 技术博客系列 (八):# 深入剖析 Swarm—— 智能体协作的新范式

AutoGen 技术博客系列 (九):从 v0.2 到 v0.4 的迁移指南

在之前的文章中,我们介绍了 AutoGen 的基本概念和使用方法。本文将深入探讨如何利用 AutoGen 的高级特性,创建更加灵活、强大的自定义智能体,并通过实战案例进行详细讲解。

AutoGen 自定义智能体高级特性概述

1. 状态管理 (State Management)

自定义智能体的状态管理是其实现复杂交互功能的关键特性之一。智能体的内部状态包含对话历史、模型上下文等重要信息,这些信息对于智能体在多次交互中保持连贯的记忆和行为一致性至关重要。在实际应用场景中,以问答系统为例,智能体需要记住用户之前的问题,才能更好地理解后续问题的背景,给出更准确、更符合逻辑的回答。

从源码层面来看,AssistantAgent类中存在与状态管理相关的操作。在处理消息时,它会根据对话历史构建提示信息,这一过程依赖于对之前消息的记忆和处理。当智能体接收到一系列与某个主题相关的问题时,它会结合之前的问题和回答,生成更具针对性的提示,从而让 LLM 生成更合适的回复。这种机制使得智能体在多轮对话中能够表现得更加智能和连贯。

若要自定义状态的保存和加载行为,开发者需重写save_state()load_state()方法。例如,在某些对数据安全性和隐私性要求较高的场景中,开发者可能希望对保存的状态数据进行加密处理。通过重写save_state()方法,可以在保存状态数据前对敏感信息进行加密;而在load_state()方法中,则需要相应地进行解密操作,以确保数据的安全性,同时满足不同应用场景下对数据处理的特殊需求。

2. 消息处理 (Message Handling)

自定义智能体主要通过on_messages()on_messages_stream()这两个关键方法来处理接收到的消息,它们在智能体的交互过程中起着核心作用。on_messages()方法接收消息序列后,会对其进行综合处理,并返回相应的响应。在处理复杂的任务请求时,该方法会分析消息内容,调用 LLM 进行推理和生成,还可能根据情况调用相关工具,最终整合结果生成回复。而on_messages_stream()方法则侧重于实现流式返回消息和事件,这在需要实时交互的场景中具有重要意义。在聊天机器人应用中,用户输入问题后,希望能实时看到智能体的思考过程和部分结果,而不是等待完整的回复。on_messages_stream()方法就可以逐段返回智能体的回复内容,增强用户体验的实时性和流畅性。

produced_message_types属性用于明确智能体可以生成的消息类型,这为智能体的交互提供了规范性和可预测性。如果智能体被设定为只能生成TextMessage类型的消息,那么在开发过程中,其他组件就可以基于这一特性进行相应的优化和处理,提高系统的稳定性和可靠性。

3. 工具集成 (Tool Integration)

工具集成赋予了自定义智能体强大的扩展能力,使其能够借助外部工具完成各种复杂任务。在实际应用中,网络搜索工具可以帮助智能体获取最新的信息,代码执行工具能够让智能体运行代码以解决编程相关问题,数据分析工具则可用于处理和分析数据。以开发一个智能数据分析助手为例,智能体可以集成数据分析工具,当用户提出数据分析相关的问题时,智能体调用该工具对数据进行处理和分析,再将结果进行整理和总结,返回给用户。

在代码实现上,通过tools参数可以方便地将自定义函数或来自Langchain等库的工具传递给智能体。在某些场景下,工具的输出可能并非自然语言格式,这时可以通过设置reflect_on_tool_use=True来让智能体反思工具的使用过程,并生成总结性的自然语言回复。当智能体使用代码执行工具运行一段代码后,它可以对代码的执行结果进行分析和总结,以自然语言的形式告知用户代码的运行情况和结果含义,提升用户对结果的理解度。

4. 条件控制 (Condition Control)

条件控制是实现智能体灵活交互的重要手段,它使得智能体能够根据不同的条件来动态调整自身行为。TextMentionTermination可以在消息中出现特定文本时终止对话,在一个智能客服系统中,如果用户输入 “结束咨询”,就可以通过TextMentionTermination来终止当前对话流程。MaxMessageTermination则用于限制对话轮数,这在一些对交互成本有严格控制的场景中非常有用,避免对话无限进行导致资源浪费。

ExternalTermination提供了从外部控制智能体停止的功能,为多智能体协作场景中的整体流程控制提供了便利。在一个由多个智能体协同完成的项目管理任务中,当外部系统检测到项目出现异常情况时,可以通过ExternalTermination及时停止相关智能体的工作,避免错误的进一步扩大。

5. 团队协作 (Team Collaboration)

团队协作特性让自定义智能体能够参与到不同的团队中,通过与其他智能体的协作来完成复杂任务。在RoundRobinGroupChat模式下,智能体按照轮流的方式进行对话,这种模式适用于一些需要公平分配发言机会的场景,在头脑风暴会议模拟中,每个智能体依次发表自己的观点和想法,确保每个成员都有机会表达。

SelectorGroupChat则根据任务的具体情况动态选择合适的智能体来处理消息,提高任务处理的效率和准确性。在一个包含多种专业智能体的系统中,当接收到一个数学问题时,SelectorGroupChat会选择擅长数学计算和推理的智能体来解答;而当遇到文本创作任务时,则会选择具有较强文本生成能力的智能体。

Swarm能够将不同的智能体组织起来,协同完成复杂的任务。在大型项目开发中,不同的智能体可以分别负责需求分析、代码编写、测试等不同环节,通过Swarm的协调,它们能够相互配合,共同推进项目的进展。

6. 记忆功能 (Memory)

记忆功能是提升智能体交互体验和个性化服务能力的关键。自定义智能体可以利用ListMemory等组件来存储和检索用户偏好或其他重要信息,以便在后续的交互中提供个性化的响应。在一个智能推荐系统中,智能体可以通过ListMemory记录用户的浏览历史、购买偏好等信息。当用户再次访问时,智能体根据这些记忆为用户推荐符合其兴趣的商品或内容,提高推荐的精准度和用户满意度。

实战案例

案例一:倒计时智能体

这个案例展示了如何创建一个简单的倒计时智能体。

from typing import AsyncGenerator, List, Sequence, Tuple
from autogen_agentchat.agents import BaseChatAgent
from autogen_agentchat.base import Response
from autogen_agentchat.messages import AgentEvent, ChatMessage, TextMessage
from autogen_core import CancellationToken


class CountDownAgent(BaseChatAgent):
    def __init__(self, name: str, count: int = 3):
        super().__init__(name, "A simple agent that counts down.")
        self._count = count


    @property
    def produced_message_types(self) -> Sequence[type[ChatMessage]]:
        return (TextMessage,)


    async def on_messages(self, messages: Sequence[ChatMessage], cancellation_token: CancellationToken) -> Response:
        response: Response | None = None
        async for message in self.on_messages_stream(messages, cancellation_token):
            if isinstance(message, Response):
                response = message
        assert response is not None
        return response


    async def on_messages_stream(
        self, messages: Sequence[ChatMessage], cancellation_token: CancellationToken
    ) -> AsyncGenerator[AgentEvent | ChatMessage | Response, None]:
        inner_messages: List[AgentEvent | ChatMessage] = []
        for i in range(self._count, 0, -1):
            msg = TextMessage(content=f"{i}...", source=self.name)
            inner_messages.append(msg)
            yield msg
        yield Response(chat_message=TextMessage(content="Done!", source=self.name), inner_messages=inner_messages)


    async def on_reset(self, cancellation_token: CancellationToken) -> None:
        pass


async def run_countdown_agent() -> None:
    countdown_agent = CountDownAgent("countdown")
    async for message in countdown_agent.on_messages_stream([], CancellationToken()):
        if isinstance(message, Response):
            print(message.chat_message.content)
        else:
            print(message.content)


# Use asyncio.run(run_countdown_agent()) when running in a script.
import asyncio
asyncio.run(run_countdown_agent())

代码运行结果:

3...
2...
1...
Done!

代码解读:
CountDownAgent继承自BaseChatAgent,这是实现自定义智能体的基础。在on_messages_stream方法中,利用AsyncGenerator按倒计时逻辑生成一系列TextMessage,模拟倒计时效果。AsyncGenerator是 Python 中用于异步生成数据的重要机制,它允许在异步操作中逐步生成数据,而不是一次性生成所有数据,非常适合实现这种需要逐次返回结果的场景。

on_messages方法则负责调用on_messages_stream,并在迭代过程中捕获最终的Response对象。这种设计模式将消息处理的逻辑分离,使得代码结构更加清晰,易于理解和维护。当CountDownAgent接收到消息时,on_messages方法启动on_messages_stream的迭代,等待并获取最终的响应,确保整个倒计时过程的完整性和准确性。

案例二:算术运算智能体团队

这个案例展示了如何创建一个包含多个算术运算智能体的团队,并使用SelectorGroupChat选择合适的智能体执行任务。

from typing import Callable, Sequence, List
from autogen_agentchat.agents import BaseChatAgent
from autogen_agentchat.base import Response
from autogen_agentchat.conditions import MaxMessageTermination
from autogen_agentchat.messages import ChatMessage, TextMessage
from autogen_agentchat.teams import SelectorGroupChat
from autogen_agentchat.ui import Console
from autogen_core import CancellationToken
from autogen_ext.models.openai import OpenAIChatCompletionClient


class ArithmeticAgent(BaseChatAgent):
    def __init__(self, name: str, description: str, operator_func: Callable[[int], int]) -> None:
        super().__init__(name, description=description)
        self._operator_func = operator_func
        self._message_history: List[ChatMessage] = []


    @property
    def produced_message_types(self) -> Sequence[type[ChatMessage]]:
        return (TextMessage,)


    async def on_messages(self, messages: Sequence[ChatMessage], cancellation_token: CancellationToken) -> Response:
         self._message_history.extend(messages)
         assert isinstance(self._message_history[-1], TextMessage)
         number = int(self._message_history[-1].content)
         result = self._operator_func(number)
         response_message = TextMessage(content=str(result), source=self.name)
         self._message_history.append(response_message)
         return Response(chat_message=response_message)


    async def on_reset(self, cancellation_token: CancellationToken) -> None:
        pass


async def run_number_agents() -> None:
    add_agent = ArithmeticAgent("add_agent", "Adds 1 to the number.", lambda x: x + 1)
    multiply_agent = ArithmeticAgent("multiply_agent", "Multiplies the number by 2.", lambda x: x * 2)
    subtract_agent = ArithmeticAgent("subtract_agent", "Subtracts 1 from the number.", lambda x: x - 1)
    divide_agent = ArithmeticAgent("divide_agent", "Divides the number by 2 and rounds down.", lambda x: x // 2)
    identity_agent = ArithmeticAgent("identity_agent", "Returns the number as is.", lambda x: x)


    termination_condition = MaxMessageTermination(10)
    selector_group_chat = SelectorGroupChat(
        [add_agent, multiply_agent, subtract_agent, divide_agent, identity_agent],
        model_client=OpenAIChatCompletionClient(model="gpt-4o"),
        termination_condition=termination_condition,
        allow_repeated_speaker=True,
        selector_prompt=(
            "Available roles: \n {roles} \n Their job descriptions: \n {participants} \n "
            "Current conversation history: \n {history} \n "
            "Please select the most appropriate role for the next message, and only return the role name."
        ),
    )
    task: List[ChatMessage] = [
        TextMessage(content="Apply the operations to turn the given number into 25.", source="user"),
        TextMessage(content="10", source="user"),
    ]
    stream = selector_group_chat.run_stream(task=task)
    await Console(stream)


# Use asyncio.run(run_number_agents()) when running in a script.
import asyncio
asyncio.run(run_number_agents())

代码运行结果:

---------- user ----------
Apply the operations to turn the given number into 25.
---------- user ----------
10
---------- multiply_agent ----------
20
---------- add_agent ----------
21
---------- multiply_agent ----------
42
---------- divide_agent ----------
21
---------- add_agent ----------
22
---------- add_agent ----------
23
---------- add_agent ----------
24
---------- add_agent ----------
25
---------- Summary ----------
Number of messages: 10
Finish reason: Maximum number of messages 10 reached, current message count: 10
Total prompt tokens: 0
Total completion tokens: 0
Duration: 2.40 seconds

代码解读:
ArithmeticAgent作为自定义智能体,接收一个数字并应用特定的操作函数。在on_messages方法中,它首先将接收到的消息添加到_message_history列表中,这一操作记录了对话的历史信息,为后续的计算和决策提供依据。接着,从最新的消息中提取数字,调用_operator_func进行运算,并将结果封装成TextMessage返回。这种设计使得ArithmeticAgent能够根据历史消息进行连续的运算,实现复杂的计算任务。

SelectorGroupChat在这个案例中起到了关键的任务分配作用,它根据对话历史和selector_prompt定义的提示信息,从多个智能体中选择合适的智能体来处理消息。selector_prompt详细描述了选择智能体的规则,它向模型提供了可用角色、角色描述以及当前对话历史等信息,引导模型选择最适合处理当前任务的智能体。allow_repeated_speaker=True这一设置允许同一个智能体连续被选中,增加了任务处理的灵活性,使得智能体团队能够根据实际情况多次调用某个智能体进行特定的运算,以达到最终的目标。

案例三:条件控制

这个实例将结合TextMentionTerminationHandoffTermination两个条件,展示如何通过组合使用不同的终止条件来控制智能体的行为。

import asyncio
from autogen_agentchat.agents import AssistantAgent
from autogen_agentchat.base import Handoff
from autogen_agentchat.conditions import HandoffTermination, TextMentionTermination
from autogen_agentchat.teams import RoundRobinGroupChat
from autogen_agentchat.ui import Console
from autogen_ext.models.openai import OpenAIChatCompletionClient


# 创建 OpenAI 模型客户端
model_client = OpenAIChatCompletionClient(
    model="gpt-4o-2024-08-06",
    # api_key="sk-...", # 可选,如果已设置 OPENAI_API_KEY 环境变量则无需设置
)


# 创建一个懒惰的助手智能体,总是将控制权移交给用户
lazy_agent = AssistantAgent(
    "lazy_assistant",
    model_client=model_client,
    handoffs=[Handoff(target="user", message="Transfer to user.")],
    system_message="Always transfer to user when you don't know the answer. Respond 'TERMINATE' when task is complete.",
)


# 定义一个终止条件,检查是否出现移交目标为 "user" 的消息和文本 "TERMINATE"
handoff_termination = HandoffTermination(target="user")
text_termination = TextMentionTermination("TERMINATE")
combined_termination = handoff_termination | text_termination


# 创建一个单智能体团队
lazy_agent_team = RoundRobinGroupChat([lazy_agent], termination_condition=combined_termination)


# 运行团队并流式输出到控制台
task = "What is the weather in New York?"
async def main():
    await Console(lazy_agent_team.run_stream(task=task))
asyncio.run(main())

代码执行过程解释

  1. 初始化智能体和团队:

    • 首先创建OpenAIChatCompletionClient连接 OpenAI 的模型,为智能体提供语言处理能力。
    • 定义AssistantAgent,名为lazy_assistant。通过handoffs参数设置移交行为,当智能体不知道答案时,它会生成一个FunctionCall,请求将控制权移交给用户,并附带消息 “Transfer to user.”。system_message则明确了智能体的行为准则,指导其在不同情况下的应对方式。
  2. 设置终止条件:

    • 定义HandoffTermination(target="user"),当智能体尝试将控制权移交给目标为 “user” 时,对话终止。
    • 定义TextMentionTermination("TERMINATE"),当智能体输出的消息中包含 “TERMINATE” 时,对话终止。
    • 使用|运算符将这两个终止条件组合成 `combined

BaseChatAgent 实现原理

BaseChatAgent是 AutoGen 中智能体的基础类,它为自定义智能体提供了重要的底层支持。通过对其源码的分析,可以深入理解 AutoGen 智能体的工作机制和拓展性设计。以下是BaseChatAgent的关键源码片段:

import asyncio
from typing import (
    TYPE_CHECKING,
    AsyncGenerator,
    List,
    Optional,
    Sequence,
    Type,
)
from autogen_core import (
    CancellationToken,
    Message,
)
if TYPE_CHECKING:
    from autogen_agentchat.models import BaseModelClient
class BaseChatAgent:
    def __init__(
        self,
        name: str,
        system_message: Optional[str] = None,
        max_consecutive_auto_reply: int = 10,
        human_input_mode: str = "NEVER",
        code_execution_config: Optional[dict] = None,
        default_auto_reply: Optional[str] = None,
        is_termination_msg: Optional[callable] = None,
        function_map: Optional[dict] = None,
        input_func: Optional[callable] = None,
        model: Optional["BaseModelClient"] = None,
        **kwargs,
    ) -> None:
        self.name = name
        self.system_message = system_message
        self.max_consecutive_auto_reply = max_consecutive_auto_reply
        self.human_input_mode = human_input_mode
        self.code_execution_config = code_execution_config
        self.default_auto_reply = default_auto_reply
        self.is_termination_msg = is_termination_msg
        self.function_map = function_map
        self.input_func = input_func
        self.model = model
    @property
    def produced_message_types(self) -> Sequence[Type[Message]]:
        raise NotImplementedError
    async def on_reset(self, cancellation_token: CancellationToken) -> None:
        pass
    async def on_message(self, message: Message, cancellation_token: CancellationToken) -> Optional[Message]:
        raise NotImplementedError
    async def on_messages(
        self, messages: Sequence[Message], cancellation_token: CancellationToken
    ) -> Optional[Message]:
        raise NotImplementedError
    async def on_messages_stream(
        self, messages: Sequence[Message], cancellation_token: CancellationToken
    ) -> AsyncGenerator[Message, None]:
        raise NotImplementedError
    async def _get_user_input(self, prompt: str, cancellation_token: CancellationToken) -> str:
        if asyncio.iscoroutinefunction(self.input_func):
            return await self.input_func(prompt, cancellation_token)
        else:
            loop = asyncio.get_event_loop()
            return await loop.run_in_executor(None, self.input_func, prompt)
  • 核心方法与功能
    • on_messages 方法:该方法是智能体处理接收到的消息的核心方法。它接收一个消息序列和一个取消令牌作为参数,在接收到消息后,智能体可以根据消息内容进行各种操作,如调用 LLM 进行推理、与其他智能体交互、执行工具等。在UserProxyAgent中,on_messages方法会根据是否接收到HandoffMessage来决定如何获取用户输入,并返回相应的消息。如果接收到HandoffMessage,则提示用户输入针对该消息的回复;否则,直接提示用户输入响应。在上述源码中,on_messages方法被定义为抽象方法,需要在子类中实现具体的逻辑,这也体现了其作为核心处理方法的通用性和可扩展性,不同的智能体可以根据自身需求定制消息处理逻辑。
    • produced_message_types 属性:定义了智能体可以产生的消息类型。这对于智能体之间的交互和消息处理非常重要,其他智能体可以根据这个属性来判断接收到的消息是否是当前智能体能够处理的类型。UserProxyAgent可以产生TextMessage和HandoffMessage,这意味着它可以发送普通文本消息和用于交接任务的消息。在源码中,produced_message_types属性同样被定义为抽象属性,需要子类去实现,以便明确该智能体能够产生的消息类型。
  • 拓展性设计体现
    • 灵活的输入函数支持:BaseChatAgent的设计允许通过input_func参数来定义自定义的输入函数。这使得智能体可以以不同的方式获取输入,例如从用户输入、文件读取、其他系统接口等。在UserProxyAgent中,如果用户提供了自定义的input_func,则智能体在获取用户输入时会调用这个函数,而不是使用默认的输入方式。这为智能体的拓展提供了很大的灵活性,开发者可以根据具体需求定制输入来源。从源码中的_get_user_input方法可以看到,它会根据input_func是否为异步函数来选择合适的方式调用输入函数,体现了对不同类型输入函数的支持。
    • 易于扩展的消息处理逻辑:on_messages方法的实现逻辑相对简单,主要是调用其他方法来完成具体的任务,如获取用户输入、处理消息内容等。这使得在自定义智能体时,可以方便地重写on_messages方法,添加额外的消息处理逻辑。可以在处理消息前对消息进行预处理,或者在处理消息后进行结果的后处理。在一个需要对用户输入进行安全过滤的场景中,可以重写on_messages方法,在获取用户输入前对输入进行过滤,确保输入的安全性。由于on_messages在基类中为抽象方法,子类重写时可以完全按照自身需求进行逻辑扩展。
    • 支持多种消息类型:通过produced_message_types属性支持多种消息类型,使得智能体可以与不同类型的智能体进行交互,适应不同的应用场景。在一个多智能体协作的系统中,不同的智能体可能产生和处理不同类型的消息,BaseChatAgent的这种设计可以确保智能体之间的消息交互顺畅。负责数据处理的智能体可能产生DataMessage类型的消息,而负责结果展示的智能体可以处理这种类型的消息并进行可视化展示。源码中通过抽象属性produced_message_types为支持多种消息类型提供了基础,子类实现该属性即可表明自身支持的消息类型。

拓展性设计的优势与应用场景

  • 优势
    • 高度定制化:开发者可以根据具体的应用需求,通过继承BaseChatAgent并定制其方法和属性,创建出满足特定业务逻辑的智能体。在一个金融交易系统中,可以创建一个专门的智能体来处理交易订单,根据市场行情和交易规则进行订单的生成、修改和执行。
    • 易于集成外部工具和系统:由于其灵活的设计,智能体可以方便地集成各种外部工具和系统,如数据库、数据分析工具、API 接口等。这使得智能体能够利用外部资源来增强自身的能力,实现更复杂的功能。在处理数据分析任务时,智能体可以调用数据分析工具对数据进行清洗、统计和可视化分析。
    • 适应复杂的业务流程:通过智能体之间的协作和对话,可以构建出适应复杂业务流程的系统。多个智能体可以分工合作,完成从任务规划、执行到结果反馈的整个过程。在一个项目管理系统中,不同的智能体可以分别负责项目计划制定、任务分配、进度跟踪和风险评估等工作,通过协作实现项目的高效管理。
  • 应用场景
    • 智能客服与聊天机器人:可以创建智能体来处理用户的咨询和问题,通过与用户的对话提供准确的答案和解决方案。智能体可以根据用户的历史记录和偏好,提供个性化的服务。在电商平台的智能客服中,智能体可以根据用户的购买历史推荐相关的商品,解答用户关于商品的疑问。
    • 自动化任务执行:在一些重复性的任务中,如数据处理、文件整理、报告生成等,可以使用智能体来自动化执行这些任务。智能体可以按照预设的规则和流程,自动完成任务,提高工作效率。在财务报表生成的场景中,智能体可以从数据库中提取数据,进行计算和分析,然后生成财务报表。
    • 多智能体协作系统:在需要多个智能体协作完成任务的场景中,如智能交通系统、智能物流系统等,BaseChatAgent的拓展性设计可以确保不同智能体之间的有效协作。在智能交通系统中,交通管理智能体、车辆智能体和路况监测智能体可以通过协作,实现交通流量的优化、车辆的智能调度和路况的实时监测。