为了账号安全,请及时绑定邮箱和手机立即绑定

🤖 用CopilotKit、LangGraph和Google地图API打造智能旅行规划助手 🤩

TL;DR(简述)

在这个简单的教程中,我们将使用CopilotKit来增强这个简单的旅行规划工具,并加入人工智能功能。

读完本文后,你会学到:

  • 代理型副驾是什么样的,以及如何利用它将AI功能添加到您的应用中。
  • 如何让副驾更新应用程序状态并在实时更新显示。
  • 如何使用 useCoAgentStateRender 实现人机协作的工作流程。

💁 喜欢通过视频学习吗?可以看看这个视频教程。
https://youtu.be/9v3kXiOY3vg?si=b7hBDCMx5Xu44MIj

下面是我们即将构建的应用程序的预览:👇

💁 试试这个旅行规划器演示,访问这个链接

……此处省略若干字……

查看仓库页面

在这次演示中,我们将从一个包含基本功能、不支持AI的应用程序的分支开始。我们将使用CopilotKit来增强这个应用。🚀

试试 CopilotKit ⭐️

查看起始分支

我们将从名为 coagents-travel-tutorial-start 的分支开始,该分支包含了我们旅行应用的初始代码:

下面的命令将会克隆一个特定分支的GitHub仓库到你的本地机器.
git clone -b coagents-travel-tutorial-start https://github.com/CopilotKit/CopilotKit.git
cd CopilotKit
执行完上述命令后,请确保你已经切换到 `CopilotKit` 目录.

全屏模式;退出全屏

教程中的代码位于 examples/coagents-travel 目录中,这两个目录分别是其中包含的两个不同的子目录。

  • ui/: 这里有一个 Next.js 应用程序,我们将在其中集成 LangGraph 代理。

  • agent/: 它存放了一个使用Python开发的LangGraph代理,

进入 examples/coagents-travel 文件夹:

cd examples/coagents-travel

全屏 全屏退出

安装依赖项

让我们来配置 Next.js 应用程序。确保您的系统上已经安装了 pnpm,因为我们采用的启动代码使用 pnpm 作为包管理器。

在终端中输入以下命令来全局安装最新版本的 pnpm(10 版本):npm install -g pnpm@latest-10

全屏模式, 退全屏

现在,进入 ui 文件夹并安装所有必要的依赖:

    cd ui
    pnpm install

在上述命令中,首先切换到ui目录,然后使用pnpm安装所需的依赖。

点击全屏按钮可以进入全屏模式,点击退出全屏按钮可以退出全屏模式

获取 API 密钥

ui 目录中创建一个 .env 文件,并填入所需的环境变量

    # 👇 ui/.env

    OPENAI_API_KEY=<你的openai_api_key>
    NEXT_PUBLIC_CPK_API_KEY=<你的公共copilotkit_api_key>

全屏模式 退出全屏

如果你需要CopilotKit API密钥,你可以在这里领取。这里 🔑

启动项目

现在,我们可以启动开发服务器了。

运行开发环境的命令是 `pnpm run dev`。

进入全屏,退出全屏

如果一切设置正确的话,访问http://localhost:3000看看你的旅行应用。希望你会喜欢。 😻

现在,让我们来看看LangGraph程序是如何工作的。

……

语言图谱代理

在我们开始集成LangGraph代理之前先,花点时间了解它是如何工作的。

在这个教程中,我们不会从零开始构建 LangGraph 代理程序。相反,我们将使用该版本,它位于 agent 目录。

💁 想了解更多关于构建LangGraph代理的详细步骤指南?请查看LangGraph快速入门

我们先来看看LangGraph代理内部是怎么运作的,然后再把它加到我们的程序里。

安装 LangGraph Studio

💡 LangGraph Studio 是一个出色的工具,用于可视化和调试 LangGraph 工作流程。虽然使用 CopilotKit 并不需要这个工具,但非常推荐使用它来理解 LangGraph 的运行机制。

要安装 LangChain Studio,可以参考安装指南

获取API密钥

agent 目录下创建一个 .env 文件,并在里面放入以下环境变量设置:

    # 👇 agent/.env

    OPENAI_API_KEY=请填写你的openai_api_key
    GOOGLE_MAPS_API_KEY=请填写你的google_maps_api_key

全屏模式 退出全屏 模式

需要 Google 地图 API 密钥吗?按照这个指南(点击这里^1)获取它。^1

LangGraph代理的可视化

安装了 LangGraph Studio 后,您可以打开工作室中的 examples/coagents-travel/agent 目录,来加载并可视化 LangGraph 智能代理。

💡 小贴士: 设置好一切可能需要一点时间,但一旦安装好并进入 agent 文件夹之后,可视化会看起来像这样:

LangGraph 可视化

注意:链接部分保持不变,因为链接不可翻译。

试试 LangGraph 代理

要测试LangGraph代理程序,只需将消息添加到变量中,然后点击“提交按钮”。

智能代理将处理收到的输入信息,在聊天中作出回应,并通过连接的节点遵循预定的工作流程。

在这个例子中,智能体触发了 search_node 来触发搜索。一旦获取到响应,它会使用 trips_node 来更新状态,根据找到的信息添加一个新的行程。🎯

理解断点

我们来谈谈代理驾驶助手中的一个重要概念:人在回路中

想象你的代理非常乐于助人,但有时可能有点过头。断点就像是一个友好的暂停按钮,让用户在代理采取行动之前批准其决定,以防它失控或者仅仅犯错。LangGraph利用断点使这变得简单。

它是这样工作的,

  • 点击 trips_node,然后开启 interrupt_after 选项。

    这次,尝试让代理创建一个新的行程。它会在执行中途停下来并征得你的同意。

你看,连AI也能学会讲礼貌。😉

LangGraph Studio 的进度情况

让 LangGraph Studio 一直运行

你的代理需要一个家,目前就住在这里的LangGraph Studio。让它在本地运行,你可以在应用程序左下角看到它的网址。

语言图谱工作室链接

我们稍后会用这个URL来连接LangGraph代理程序与我们的CopilotKit。

🤖到目前为止,我们做得非常出色。现在让我们把这一切应用到实际中,通过将LangGraph代理集成到我们的旅行应用里,作为代理来实现。一起出发吧!

等等

如何配置 CopilotKit

现在到了有趣的部分——让我们添加CopilotKit,将所有东西整合起来。因为我们已经有了应用程序和代理在运行,只需要一步就能把CopilotKit集成到我们的应用里了。

在本教程中,我们将安装以下依赖包。

  • @copilotkit/react-core: CopilotKit的核心库,主要提供CopilotKit提供器和实用的钩子。

  • @copilotkit/react-ui: CopilotKit 的 UI 库,包含侧边栏、聊天弹出框、文本框等,例如侧边栏、聊天弹出框、文本框等组件。

安装依赖

首先,如果你还没有在 ui 目录里,请切换到 ui 目录。

    cd ../ui

全屏 退出全屏

然后,安装 CopilotKit 的包(或组件)。

在终端中输入以下命令:

pnpm add @copilotkit/react-core @copilotkit/react-ui  

点击全屏按钮可以切换到全屏模式,再点击一次可以切换回正常模式

这两个包就足以在React应用中安装CopilotKit。@copilotkit/react-core包含了CopilotKit的核心功能,而@copilotkit/react-ui则包含了一些可以直接使用的预构建UI组件。

添加代码助手套件

要配置 CopilotKit,有兩種方式:

  • Copilot Cloud:快速上手,简单易用,并且完全托管。

  • 自己托管:更自由的控制权,但也增加了额外的复杂性。

在这个教程中,我们会走云端路线(因为目前为什么还要麻烦自己去处理额外的复杂性),但如果你有兴趣的话,也可以自己搭建。如果你想自己来,可以看看自我托管指南

设置 Copilot 云端

这是开始使用Copilot Cloud的方法:首先,你可以...

  • 创建账户

前往 Copilot Cloud 并注册一下。这只需要大约一分钟的时间。

  • 获取你的API密钥

登录之后,按照屏幕上的指示操作以获取您的Copilot Cloud 公共 API 密钥。您还需要获取一个 OpenAI API 密钥。

设置你的 OpenAI API 密钥后,然后点击对勾,就这样简单,你就能拿到你的公钥了。

CopilotKit 云端界面

  • 将 API密钥添加到你的 .env

ui 目录中的 .env 文件里更新你的 Copilot Cloud API 密钥:并在文件中加入你的 API 密钥。

    # 👇 ui/.env

    # 剩余的环境变量...
    NEXT_PUBLIC_CPK_PUBLIC_API_KEY=<你的copilotkit_public_key>

全屏 / 退出全屏

  • 设置 CopilotKit 供应商

现在,要在您的应用程序中集成CopilotKit,只需将其包裹在CopilotKit提供器内。

通过使用提供者包装我们的应用,我们确保了即将从 @copilotkit/react-ui 组件添加的其他 UI 组件能够与 CopilotKit SDK 顺利交互。

编辑 ui/app/page.tsx:添加以下代码:

    // 👇 ui/app/page.tsx

    "use client";

    // 其他导入语句...
    import { CopilotKit } from "@copilotkit/react-core"; 

    // 其余代码...

    export default function Home() {
      return (
        <CopilotKit
          publicApiKey={process.env.NEXT_PUBLIC_CPK_PUBLIC_API_KEY}
        >
          <TooltipProvider>
            <TripsProvider>
              <main className="h-screen w-screen">
                <MapCanvas />
              </main>
            </TripsProvider>
          </TooltipProvider>
        </CopilotKit> 
      );
    }

点击全屏按钮来全屏显示,想要退出的话点击退出全屏模式按钮。

  • 助手工具包 用户界面 组件

CopilotKit 配备了几个即插即用的组件,如 <CopilotPopup/><CopilotSidebar/>。只需放置这些组件,它们就会看起来非常棒。

如果你不想使用内置组件也没关系!CopilotKit 支持无头模式,使用 useCopilotChat,所以如果你喜欢动手,可以完全自己动手一番。😉

在这篇教程中,我们将使用 <CopilotSidebar /> 组件显示聊天侧边栏。同样的方法适用于其他任何预建的 UI 组件。

编辑 ui/app/page.tsx 文件,添加 <ChatSidebar /> 组件,并确保 CSS 样式已导入。

    // 👇 ui/app/page.tsx

    "use client";

    // Rest of the imports...

    import { TasksList } from "@/components/TasksList";
    import { TasksProvider } from "@/lib/hooks/use-tasks";
    import { CopilotKit } from "@copilotkit/react-core";
    import { CopilotSidebar } from "@copilotkit/react-ui"; 
    import "@copilotkit/react-ui/styles.css"; 

    // Rest of the code...

    export default function Home() {
      return (
        <CopilotKit
          publicApiKey={process.env.NEXT_PUBLIC_CPK_PUBLIC_API_KEY}
        >
          <CopilotSidebar
            defaultOpen={true}
            clickOutsideToClose={false}
            labels={{
              title: "行程规划器",
              initial: "嗨!👋 我在这里帮你规划旅行。我可以帮你管理旅行,添加地点,或者一般地帮你规划一个新的行程。",
            }}
          />
          <TooltipProvider>
            <TripsProvider>
              <main className="h-screen w-screen">
                <MapCanvas />
              </main>
            </TripsProvider>
          </TooltipProvider>
        </CopilotKit>
      );
    }

切换到全屏模式 退出全屏

首先,我们导入所需的模块和自定义样式,以便侧边栏看起来很棒。👌 然后,加入 <CopilotSidebar /> 组件。

<CopilotSidebar /> 组件中,你可以通过传递 labels 属性来更改标题和 AI 的初始消息。

现在回到你的应用。往右边看,voilà! 出现了闪亮的新聊天侧边栏,只需要几行代码就可以使用。 😻

CopilotKit 侧边栏

不过,还是缺少一些东西,那就是副驾具备决策能力。我们会通过位于asset目录中的LangGraph来增加这个功能。

此处省略内容
让你的副驾更主动一些

我们在 LangGraph Studio 运行着一个 LangGraph 代理,还有一个功能性但不够聪明的副驾,但它还没有那么聪明。让我们给这个副驾一些真正的决策能力!让它变得更有能力吧!😎

快速了解 React 状态管理

我们快速回顾一下应用的状态管理,然后打开一下这个文件:lib/hooks/use-trips.tsx

这里有个叫TripsProvider的东西,它有很多有用的功能。真正的主角是state对象,它是按照AgentState类型定义的。

这个状态可以通过 useTrips 钩子在整个应用中可访问,为如 TripCardTripContentTripSelect 这些组件提供数据。

如果你以前用过 React 应用,这感觉应该会很熟悉——通过 context 或库来管理状态是相当标准的。

将代理和状态结合

现在到了重要部分:将我们的LangGraph代理与其状态连接起来。要实现这一点,我们将设置一个远程端点来,并使用useCoAgent钩子来实现这一魔法。🌟

  • 搭建隧道

还记得之前的 LangGraph Studio 端点吗?你现在就要用到它了!如果你使用的是 Copilot Cloud,你已经准备好了。

如果你选择了自托管这条路,请按照这里的步骤来。

为了将我们本地运行的LangGraph服务与Copilot Cloud连接起来,我们首先找到LangGraph Studio端点的端口号,然后使用CopilotKit CLI。

💁 LangGraph Studio的位置:你可以在LangGraph Studio界面左下角找到它。

LangGraph Studio 端点

现在来,启动你的终端,然后运行这个命令:

# 将 <port_number> 占位符替换为实际的端口数字
npx @copilotkit/cli tunnel <port_number>

进入全屏,退出全屏

Boom! 你打通了一个隧道。🎉 你将看到以下内容:

✔ 隧道创建成功!  
隧道详情:  
本地:localhost:54209  
公开 URL:https://light-pandas-argue.loca.lt  
按 Ctrl+C 终止隧道

全屏 退出全屏

保存这个公共URL哦。🔗 它将成为我们在本地运行的LangGraph代理程序和CopilotKit Cloud之间的桥梁。它将连接我们在本地运行的LangGraph代理和CopilotKit Cloud。

💁 接下来,我们需要 LangSmith API Key(LangSmith API密钥)。请参阅此指南来获取它。

  • 将隧道连接至Copilot云端

访问Copilot Cloud,在页面上滚动找到Remote Endpoints部分,然后点击+ 添加新按钮。

  • 选择 LangGraph 平台。
  • 添加公共 URL(从 CopilotKit CLI 生成的 URL),并添加你的 LangSmith API 密钥。
  • 点击 创建

🎉 完成!您的代理端点现在已列出,当调用该代理时,CopilotKit 知道确切地向何处发送请求。

  • 锁住代理

因为我们这里只有一个代理,让我们确保将 <CopilotKit /> 提供者锁定,以便所有请求都只能发送给这个特定代理。要添加代理,只需在属性中加入 agent 的名字即可。

💁 对处理多个代理好奇吗?可以查看多代理概念指南

    // 👇 ui/app/page.tsx

    // Rest of the code...
    <CopilotKit
      // Rest of the code...
      agent="旅游"
    >
        {/* Rest of the code... */}
    </CopilotKit>

全屏模式 退出全屏

我们把这种程序或角色称为travel,因为已经在agents/langgraph.json文件中定义。

就这样,副驾驶现在变得更主动了。它不仅能聊天,还能做决定。这也太酷了吧!🤯

设置代理和状态

现在,我们希望将LangGraph代理的状态与我们应用的状态关联起来。这将使我们能够实现实时动态交互。

LangGraph代理会记录自己的状态,正如你在LangGraph工作室界面左下角已经看到的那样。

🤔 现在是什么想法呢?

我们希望实现这些状态间的双向互动。为了实现这一点,友好的钩子useCoAgent正好能帮我们实现这一点,来自CopilotKit。

打开并编辑 ui/lib/hooks/use-trips.tsx 文件,在文件中添加以下代码以在文件中添加 useCoAgent 钩子。

/*
    // 👇 ui/lib/hooks/use-trips.tsx

    // 其他导入...
    import { AgentState, defaultTrips } from "@/lib/trips"; 
    import { useCoAgent } from "@copilotkit/react-core"; 

    export const TripsProvider = ({ children }: { children: ReactNode }) => {
      const { state, setState } = useCoAgent<AgentState>({
        name: "travel",
        初始状态: {
          trips: defaultTrips,
          所选行程ID: defaultTrips[0].id,
        },
      });

      // 其余代码...

}

切换到全屏 退出全屏

没错,就是这样。这就够同步这两个状态了。ὕ

接下来,让我们逐行分析代码,看看每行的意思。

💡 useCoAgent 钩子是通用的,这意味着你可以指定一个与 LangGraph 代理状态相似的类型。
在这一例子中,我们使用 AgentState 以保持一定的统一性,虽然可以勉强将其类型强制转换为 any 类型,但这通常不是一个好实践。还是尽量不要这么干。

name 参数将所有内容与你图中 agent/langgraph.json 定义的名称关联起来。确保正确命名,这样代理和我们应用才能始终保持同步。

initialState 中,我们使用 defaultTrips(来自 @/lib/types.ts),不过这并不是必需的。

我们先加几个初始行程,以便立即测试它是否正常工作。

这是初始状态,即 defaultTrips,如下所示。

    // 👇 ui/lib/types.ts

    export const defaultTrips: Trip[] = [
      {
        id: "1",
        name: "纽约商务旅行",
        center_latitude: 40.7484,
        center_longitude: -73.9857,
        places: [
          {
            id: "1",
            name: "中央公园",
            address: "纽约市,NY 10024",
            description: "位于纽约市著名的公园",
            latitude: 40.785091,
            longitude: -73.968285,
            rating: 4.7,
          },
          {
            id: "3",
            name: "时代广场",
            address: "纽约市,NY 10036",
            description: "位于纽约市著名的广场",
            latitude: 40.755499,
            longitude: -73.985701,
            rating: 4.6,
          },
        ],
        zoom_level: 14,
      },
      // 其余行程...
    ];

切换到全屏,退出全屏

来吧,该试试它了!

打开你的应用,问智能助手关于你的旅行的任何问题。

我有几次出行记录?

全屏切换 退出全屏

看到代理是怎么从应用状态里提取数据的吗?这简直是魔法,对吧? 😻

状态由应用程序和代理共同管理,所以试着手动删除或编辑一趟旅行,再问一遍,它应该会给出相应的答复:

我现在有什么安排?

进入全屏;退出全屏

这个代理很懂行情。更棒的是,你可以直接给他安排任务:

帮我添加一些酒店到我在巴黎的行程

全屏模式 退出全屏

就这样!状态更新了,你的界面也会随之更新。

目前,应用的核心功能已经完成了。我们只需要提升一下用户体验就可以了,通过增加实时显示的文字以及其他功能,来为用户提供实时反馈。


实时响应显示

现在我们可以与代理互动,获取和更新数据,为什么不加个文本流来增强用户体验?

这就像实时观看代理工作时的实时进度,就像你看其他很多流行AI一样,比如 ChatGPT

在这一步中,我们将实现 copilotkit_emit_state SDK 函数,以便 LangGraph 代理在工作时不断更新进度。这样,代理在工作时会不断更新进度。🔥

让我们开始安装CopilotKit插件吧

首先,让我们来安装CopilotKit SDK。由于这里我们使用的是一个基于Python的代理软件(并且用poetry来管理它),所以我们将安装Python版本的SDK。

💁 不确定如何安装 poetry?可以在这里找到安装指南 这里

    poetry add copilotkit==0.1.31a4

在 poetry 中添加 copilotkit 版本 0.1.31a4

全屏模式。退出全屏

既然我们要编辑search_node,我们就直接打开search.py文件。

手动触发代理程序的状态

使用 CoAgents 时,当节点发生变化(即,当遍历边时)会发出代理的状态信息。但是如果我们希望在执行动作的过程中显示进度呢?好消息就是,我们可以通过手动调用 copilotkit_emit_state 来发送状态信息。

让我们给 search_node 添加一个自定义配置,这样我们就可以输出中间的计算状态。

打开 agent/travel/search.py 文件,并在里面加入以下代码行:

    # 👇 agent/travel/search.py

    # 其余导入...
    from copilotkit.langchain import copilotkit_emit_state, copilotkit_customize_config 

    async def search_node(state: AgentState, config: RunnableConfig):
        """
        搜索节点用于搜索地点。
        """
        ai_message = cast(AIMessage, state["messages"][-1])

        config = copilotkit_customize_config(
            config,
            emit_intermediate_state=[{
                "state_key": "搜索进度状态",
                "tool": "搜索地点工具",
                "tool_argument": "搜索进度状态",
            }],
        )

        # 其余代码...

全屏 退出全屏

发出中间状态

现在,让我们用 copilotkit_emit_state 手动发送状态,你会看到我们每次发送查询时的状态更新。

让我们再编辑一下 agent/travel/search.py,在搜索开始时以及在结果返回时发出状态信息。

agent/travel/search.py文件中添加以下代码,如下:

    # 👇 agent/旅行/搜索.py

    # 其余代码...
    async def 搜索节点函数(state: AgentState, config: RunnableConfig):
        """
        搜索节点函数负责搜索地点。
        """
        ai_message = cast(AIMessage, state["messages"][-1])

        config = 自定义配置CoPilotKit(
            config,
            发送中间状态=[{
                "状态键": "搜索进度信息",
                "工具": "搜索地点功能",
                "工具参数": "搜索进度信息",
            }],
        )

        # ^ 之前的代码

        state["搜索进度信息"] = state.get("搜索进度信息", [])
        查询列表 = ai_message.工具调用[0]["args"]["queries"]

        for query in 查询列表:
            state["搜索进度信息"].append({
                "查询": query,
                "结果": [],
                "已完成": False
            })

        await 发送状态CoPilotKit(config, state) 

        # 其余代码...

全屏 切换全屏

更新并显示进度

现在我们可以实时展示结果了,在搜索过程中我们会实时更新进度。

在文件 agent/travel/search.py 中用以下代码进行更新:

    # 👇 agent/旅行/搜索.py

    # 未列出的代码...
    async def 搜索节点函数(state: AgentState, config: RunnableConfig):
        """
        搜索节点用于搜索地点。
        """
        ai_message = cast(AIMessage, state["messages"][-1])

        config = copilotkit_自定义配置(
            config,
            发送中间状态信息=[{
                "状态键": "搜索进度",
                "工具": "搜索地点",
                "工具参数": "搜索进度",
            }],
        )

        state["搜索进度"] = state.get("搜索进度", [])
        查询 = ai_message.工具调用请求[0]["args"]["queries"]

        for query in 查询:
            state["搜索进度"].append({
                "查询": query,
                "结果": [],
                "是否完成": False
            })

        await copilotkit_发送状态(config, state) 

        # 先前的代码

        地点信息 = []
        for i, query in enumerate(查询):
            响应结果 = gmaps.places(query)
            for 结果 in 响应结果.get("results", []):
                地点信息 = {
                    "id": 结果.get("place_id", f"{结果.get('name', '')}-{i}"),
                    "名称": 结果.get("name", ""),
                    "地址": 结果.get("formatted_address", ""),
                    "纬度坐标": 结果.get("geometry", {}).get("location", {}).get("lat", 0),
                    "经度坐标": 结果.get("geometry", {}).get("location", {}).get("lng", 0),
                    "评分": 结果.get("rating", 0),
                }
                地点信息.append(地点信息)
            state["搜索进度"][i]["是否完成"] = True
            await copilotkit_发送状态(config, state) 

        state["搜索进度"] = []
        await copilotkit_发送状态(config, state) 

        # 后续的代码...

切换到全屏模式 退出全屏模式

显示进度

为了显示 UI 中的进度,我们将使用 useCoAgentStateRender 这个钩子。这个钩子会根据条件渲染 search_progress 状态。

我们只需要通过 useCoAgentStateRender 钩子根据条件来渲染 search_progress 状态值。

现在我们来修改 ui/lib/hooks/use-trips.tsx 来显示搜索的进度:

    // 👇 ui/lib/hooks/use-trips.tsx

    // 其他导入...
    import { useCoAgent, useCoAgentStateRender } from "@copilotkit/react-core"; 
    import { SearchProgress } from "@/components/SearchProgress"; 

    export const TripsProvider = ({ children }: { children: React.ReactNode }) => {
      // 其他代码...

      useCoAgentStateRender<AgentState>({
        name: "travel",
        render: ({ state }) => {
          if (state.search_progress) {
            return <SearchProgress progress={state.search_progress} />;
          }
          return null;
        },
      });

      // 其他代码...
    }

全屏 退出全屏

<SearchProgress /> 组件已经为你准备好了。如果你对它感兴趣,可以看看 ui/components/SearchProgress.tsx 中的实现。 🙌

💁 额外福利search_progress 状态键值已经在 ui/lib/types.ts 文件的 AgentState 类型中预定义好,所以你无需从零开始创建它。

现在,试试看吧!向代理提问吧,你就能实时看到进度更新了。🤩


通过加入人机协作来增加控制性

好吧,现在来点真家伙。如果代理即将做出你不同意的决定,该怎么办?

人机协作循环 (HITL) 允许你批准、拒绝或修改智能体想要执行的行动。

我们将在这里设置一个“断点”,让代理暂停,等待你的批准再继续。

💁 对断点感到好奇吗?可以在这里了解更多详情 点击这里

一旦断点被命中,我们会将其发送到前端界面,用户会批准或拒绝该操作。然后,代理将根据用户的决定继续操作。整个过程会是这样的:

查看下方的信息图,看看整个流程是怎么运作的:👇

Coagents HITL 信息图

  • 添加断点以供人工介入

使用我们的LangGraph,添加人工介入功能非常直接。trips_node作为perform_trips_node的中介,我们可以在trips_node处设置断点,从而暂停执行。

agent/travel/agent.py 文件中,指示它在何处插入暂停。具体来说,我们在 compile 函数中这样做。

    # 👇 agent/旅行/agent.py

    # 其余的代码...

    graph = graph_builder.compile(
        checkpointer=MemorySaver(),
        # 暂停在这里,等用户回复!
        interrupt_after=["行程节点"], 
    )

全屏 退出全屏

现在,这样做了以后,代理就会问我们,“我是否应该继续?”而不是盲目地继续下去。

  • 应对用户的决定

当用户点击暂停按钮时,我们得看看他们想干嘛。他们是不是同意这一步骤,还是点了“取消”然后重新来过?我们要搞清楚他们想干嘛。

perform_trips_node 这个步骤里,我们将获取工具消息,看看用户做了什么选择。

    # 👇 agent/旅行/旅行.py

    # 其余代码...

    async def 处理旅行节点(state: AgentState, config: RunnableConfig):
        """处理旅行任务"""
        ai_message = cast(AI消息, state["messages"][-2]) 
        tool_message = cast(工具消息, state["messages"][-1]) 

        # 余下的代码...

点击以进入全屏模式,再次点击退出

条件判断会检查用户的决定然后根据决定作出相应反应。

如果用户说“取消”的话,我们就停止所有操作并直接返回自定义消息。否则,对于其他任何回复,会继续处理。

    # 👇 agent/旅行/行程.py

    # 代码其余...
    async def 处理行程节点(state: AgentState, config: RunnableConfig):
        """处理行程操作"""
        ai_message = cast(AIMessage, state["messages"][-2])
        工具消息对象 = cast(ToolMessage, state["messages"][-1])

        if 工具消息对象.content == "CANCEL":
          return {
            "messages": AIMessage(content="已取消行程操作。"),
          }

        # 处理边缘情况,即AI消息不是AIMessage或没有工具调用,这种情况理论上不应发生。
        if not isinstance(ai_message, AIMessage) or not ai_message.tool_calls:
            return state

        # 代码其余...

点击全屏 点击退出全屏

  • 显示决策界面

现在是时候更新前端以显示工具调用并捕获用户的决定选择,传递给代理。为此,我们将会使用带有 renderAndWait 选项的 useCopilotAction 钩子。

编辑 ui/lib/hooks/use-trips.tsx 文件,并添加如下代码行:

    // 👇 ui/lib/hooks/use-trips.tsx

    // 其余导入...
    import { AddTrips, EditTrips, DeleteTrips } from "@/components/humanInTheLoop";
    import { useCoAgent, useCoAgentStateRender, useCopilotAction } from "@copilotkit/react-core";

    // 其余代码...

    export const TripsProvider = ({ children }: { children: ReactNode }) => {
      // 其余代码...

      useCoAgentStateRender<AgentState>({
        name: "travel",
        render: ({ state }) => {
          return <SearchProgress progress={state.search_progress} />;
        },
      });

      useCopilotAction({
        name: "add_trips",
        description: "添加一些行程",
        parameters: [
          {
            name: "trips",
            type: "object[]",
            description: "需要添加的行程",
            required: true,
          },
        ],
        renderAndWait: AddTrips,
      });

      useCopilotAction({
        name: "update_trips",
        description: "更新一些行程",
        parameters: [
          {
            name: "trips",
            type: "object[]",
            description: "需要更新的行程",
            required: true,
          },
        ],
        renderAndWait: EditTrips,
      });

      useCopilotAction({
        name: "delete_trips",
        description: "删除一些行程",
        parameters: [
          {
            name: "trip_ids",
            type: "string[]",
            description: "需要删除的行程ID(必须提供)",
            required: true,
          },
        ],
        renderAndWait: (props) => <DeleteTrips {...props} trips={state.trips} />,
      });

      // 其余代码...

全屏模式 退出全屏

有了这套配置,前端已经准备好渲染工具调用的界面并捕捉用户的操作决定。不过,还有一个重要的问题我们还没提到:我们怎样处理用户的输入并回传给代理?

  • 可选:了解人在环中部分

我们快速了解一下前端是如何处理这个情况的。我们将使用 DeleteTrips 组件作为示例,但相同的逻辑也适用于 AddTripsEditTrips

    // 👇 ui/lib/components/humanInTheLoop/DeleteTrips.tsx

    import { Trip } from "@/lib/types";
    import { PlaceCard } from "@/components/PlaceCard";
    import { X, Trash } from "lucide-react";
    import { ActionButtons } from "./ActionButtons"; 
    import { RenderFunctionStatus } from "@copilotkit/react-core";

    export type DeleteTripsProps = {
      args: any;
      status: RenderFunctionStatus;
      handler: any;
      trips: Trip[];
    };

    export const DeleteTrips = ({ args, status, handler, trips }: DeleteTripsProps) => {
      const tripsToDelete = trips.filter((trip: Trip) => args?.trip_ids?.includes(trip.id));

      return (
        <div className="space-y-4 w-full bg-secondary p-6 rounded-lg">
          <h1 className="text-sm">以下行程将被删除:</h1>
          {status !== "complete" && tripsToDelete?.map((trip: Trip) => (
            <div key={trip.id} className="flex flex-col gap-4">
              <>
                <hr className="my-2" />
                <div className="flex flex-col gap-4">
                  <h2 className="text-lg font-bold">{trip.name}</h2>
                  {trip.places?.map((place) => (
                    <PlaceCard key={place.id} place={place} />
                  ))}
                </div>
              </>
            </div>
          ))}
          { status !== "complete" && (
            <ActionButtons
              status={status} 
              handler={handler} 
              approve={<><Trash className="w-4 h-4 mr-2" /> 确认删除</>} 
              reject={<><X className="w-4 h-4 mr-2" /> 取消删除</>} 
            />
          )}
        </div>
      );
    };

全屏 开 全屏 关

这里的关键在于 ActionButtons 组件,它允许用户批准,或拒绝这个操作。这就是用户做出决定的地方。

    // 👇 ui/lib/components/人在循环中/ActionButtons.tsx

    import { RenderFunctionStatus } from "@copilotkit/react-core";
    import { Button } from "../ui/button";

    export type ActionButtonsProps = {
        status: RenderFunctionStatus;
        handler: any;
        approve: React.ReactNode;
        reject: React.ReactNode;
    }

    export const ActionButtons = ({ status, handler, approve, reject }: ActionButtonsProps) => (
      <div className="flex gap-4 justify-between">
        <Button 
          className="w-full"
          variant="outline"
          disabled={status === "complete" || status === "inProgress"} 
          onClick={() => handler?.("CANCEL")} 
        >
          {reject}
        </Button>
        <Button 
          className="w-full"
          disabled={status === "complete" || status === "inProgress"} 
          onClick={() => handler?.("SEND")} 
        >
          {approve}
        </Button>
      </div>
    );

进入全屏 退出全屏

这里有两个按钮,点击“取消”按钮时,会调用 handler?.("CANCEL"),点击“删除”按钮时,会调用 handler?.("SEND")。这会把用户的选择(无论是“CANCEL”还是“SEND”)传回给代理。

重要的是,onClick 处理函数将用户的点击结果回传给代理,。

💡 如果你想允许用户在回传给代理之前编辑工具调用的参数,可以通过修改 onClick 事件处理程序并调整代理处理工具调用的方法来实现这一点。

就这样。😮‍💨 我们成功地加入了人工审核功能。现在它可以在添加、编辑或删除行程时提示用户批准或拒绝这些操作的,并将用户的决定传回代理。


结尾 ⚡

在这次教程里,希望你学会了如何使用CopilotKit为你的应用添加代理型副驾(CopilotKit),实时改变状态,同时了解人机循环的概念。

源代码:源代码

💁 你也可以看看这个项目的原始文档页面

非常感谢你来看!🎉 🫡

在下面的评论区分享你的想法!👇

猫打键盘 一只猫在打键盘。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消