从零开始学 LangChain(15) | 豆包MarsCode AI刷题

90 阅读12分钟

代理(3)

复杂的代理

Structured Tool Chat(结构化工具对话)代理、

结构化工具对话代理,允许更复杂、多方面的交互。

通过指定AgentType.STRUCTUREDCHATZEROSHOTREACT_DESCRIPTION 这个代理类型,代理能够调用包含一系列复杂工具的“结构化工具箱”,组合调用其中的多个工具,完成批次相关的任务集合。

举例来说,结构化工具的示例包括:

  1. 文件管理工具集:支持所有文件系统操作,如写入、搜索、移动、复制、列目录和查找。
  2. Web 浏览器工具集:官方的 PlayWright 浏览器工具包,允许代理访问网站、点击、提交表单和查询数据。

下面,我们就以 PlayWright 工具包为例,来实现一个结构化工具对话代理。

先来看一看什么是 PlayWright 工具包。

什么是 Playwright

Playwright是一个开源的自动化框架,它可以让你模拟真实用户操作网页,帮助开发者和测试者自动化网页交互和测试。用简单的话说,它就像一个“机器人”,可以按照你给的指令去浏览网页、点击按钮、填写表单、读取页面内容等等,就像一个真实的用户在使用浏览器一样。

Playwright支持多种浏览器,比如Chrome、Firefox、Safari等,这意味着你可以用它来测试你的网站或测试应用在不同的浏览器上的表现是否一致。

现在,一切就绪,我们可以通过Playwright浏览器工具来访问一个测试网页。

from playwright.sync_api import sync_playwright

def run():
    # 使用Playwright上下文管理器
    with sync_playwright() as p:
        # 使用Chromium,但你也可以选择firefox或webkit
        browser = p.chromium.launch()

        # 创建一个新的页面
        page = browser.new_page()

        # 导航到指定的URL
        page.goto('https://langchain.com/')

        # 获取并打印页面标题
        title = page.title()
        print(f"Page title is: {title}")

        # 关闭浏览器
        browser.close()

if __name__ == "__main__":
    run()

这个简单的Playwright脚本,它打开了一个新的浏览器实例。过程是:导航到指定的URL;获取页面标题并打印页面的标题;最后关闭浏览器。

输出如下:

Page title is: LangChain

这个脚本展示了Playwright的工作方式,一切都是在命令行里面直接完成。它不需要我们真的去打开Chome网页,然后手工去点击菜单栏、拉动进度条等。

下面这个表,我列出了使用命令行进行自动化网页测试的优势。

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/4a380ee8f4954cd28f5c40ee8d435a51~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bCP5p2o5ZCM5a2mNzY2:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjEzNzU0MDc2NzM4MDMwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1732587017&x-orig-sign=ABvT3wWnD3e3gPANUbJiBAML8r8%3D

  • 使用结构化工具对话代理

    在这里,我们要使用的Agent类型是STRUCTUREDCHATZEROSHOTREACT_DESCRIPTION。要使用的工具则是PlayWrightBrowserToolkit,这是LangChain中基于PlayWrightBrowser包封装的工具箱,它继承自 BaseToolkit类。

    PlayWrightBrowserToolkit 为 PlayWright 浏览器提供了一系列交互的工具,可以在同步或异步模式下操作。

    其中具体的工具就包括:

    https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/f3cba66958be4ff1be69ce98d8c28400~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bCP5p2o5ZCM5a2mNzY2:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjEzNzU0MDc2NzM4MDMwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1732587017&x-orig-sign=vrJXkJMxc%2ByZw2%2BOsh8AayUb6mM%3D

    下面,我们就来看看结构化工具对话代理是怎样通过组合调用PlayWrightBrowserToolkit中的各种工具,自动完成我们交给它的任务。

from langchain.agents.agent_toolkits import PlayWrightBrowserToolkit
from langchain.tools.playwright.utils import create_async_playwright_browser

async_browser = create_async_playwright_browser()
toolkit = PlayWrightBrowserToolkit.from_browser(async_browser=async_browser)
tools = toolkit.get_tools()
print(tools)

from langchain.agents import initialize_agent, AgentType
from langchain.chat_models import ChatAnthropic, ChatOpenAI

# LLM不稳定,对于这个任务,可能要多跑几次才能得到正确结果
llm = ChatOpenAI(temperature=0.5)  

agent_chain = initialize_agent(
    tools,
    llm,
    agent=AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION,
    verbose=True,
)

async def main():
    response = await agent_chain.arun("What are the headers on python.langchain.com?")
    print(response)

import asyncio
loop = asyncio.get_event_loop()
loop.run_until_complete(main())

在这个示例中,我们询问大模型,网页python.langchain.com中有哪些标题目录?

很明显,大模型不可能包含这个网页的内部信息,因此,大模型不可避免地需要通过PlayWrightBrowser工具来解决问题。

第一轮思考

代理进入AgentExecutor Chain之后的第一轮思考如下:

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/3628fa6d1cd4450a84b5465845b1feb6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bCP5p2o5ZCM5a2mNzY2:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjEzNzU0MDc2NzM4MDMwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1732587017&x-orig-sign=8NZbQBdC0PMR2gNYAWpasyTM%2FcA%3D

这里,我对上述思考做一个具体说明。

I can use the "navigatebrowser" tool to visit the website and then use the "getelements" tool to retrieve the headers. Let me do that.

这是第一轮思考,大模型知道自己没有相关信息,决定使用PlayWrightBrowserToolkit工具箱中的 navigate_browser 工具。

Action:{"action": "navigate_browser", "action_input": {"url": "[python.langchain.com"}}](link.juejin.cn/?target=htt…)

行动:通过Playwright浏览器访问这个网站。

Observation: Navigating to python.langchain.com returned status code 200

观察:成功得到浏览器访问的返回结果。

在第一轮思考过程中,模型决定使用PlayWrightBrowserToolkit中的navigate_browser工具。

第二轮思考

下面是大模型的第二轮思考。

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/5c49d1115415483dbb123973b2e87dc3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bCP5p2o5ZCM5a2mNzY2:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjEzNzU0MDc2NzM4MDMwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1732587017&x-orig-sign=35%2B25sCHK%2BeebZmcnT%2BhPJOcKIM%3D

还是对上述思考做一个具体说明。

Thought:Now that I have successfully navigated to the website, I can use the "get_elements" tool to retrieve the headers. I will specify the CSS selector for the headers and retrieve their text.

第二轮思考:模型决定使用PlayWrightBrowserToolkit工具箱中的另一个工具 get_elements,并且指定CSS selector只拿标题的文字。

Action: {"action": "get_elements", "action_input": {"selector": "h1, h2, h3, h4, h5, h6", "attributes": ["innerText"]}}

行动:用Playwright的 get_elements 工具去拿网页中各级标题的文字。

Observation: [{"innerText": "Introduction"}, {"innerText": "Get started​"}, {"innerText": "Modules​"}, {"innerText": "Model I/O​"}, {"innerText": "Data connection​"}, {"innerText": "Chains​"}, {"innerText": "Agents​"}, {"innerText": "Memory​"}, {"innerText": "Callbacks​"}, {"innerText": "Examples, ecosystem, and resources​"}, {"innerText": "Use cases​"}, {"innerText": "Guides​"}, {"innerText": "Ecosystem​"}, {"innerText": "Additional resources​"}, {"innerText": "Support"}, {"innerText": "API reference​"}]

观察:成功地拿到了标题文本。

在第二轮思考过程中,模型决定使用PlayWrightBrowserToolkit中的get_elements工具。

第三轮思考

下面是大模型的第三轮思考。

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/df550dc907084a8c83fef6db8db4967f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bCP5p2o5ZCM5a2mNzY2:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjEzNzU0MDc2NzM4MDMwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1732587017&x-orig-sign=lO1c1Se20k%2FbEF%2B%2F9rczIsn5axQ%3D

对上述思考做一个具体说明。

Thought:The headers on python.langchain.com are:

1. Introduction ... ...

2. API reference

第三轮思考:模型已经找到了网页中的所有标题。

Action:

{
  "action": "Final Answer",
  "action_input": "The headers on python.langchain.com are: 1. Introduction 2. Get started 3. Modules 4. Model I/O 5. Data connection 6. Chains 7. Agents 8. Memory 9. Callbacks 10. Examples, ecosystem, and resources 11. Use cases 12. Guides 13. Ecosystem 14. Additional resources 15. Support 16. API reference"
}

行动:给出最终答案。

AgentExecutor Chain结束之后,成功输出python.langchain.com这个页面中各级标题的具体内容。

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/17aa311a24ed4679a72c66f1eeef9217~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bCP5p2o5ZCM5a2mNzY2:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjEzNzU0MDc2NzM4MDMwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1732587017&x-orig-sign=hhYCaJ3PlF1d5lZ5X3%2BWfSbKmWM%3D

在这个过程中,结构化工具代理组合调用了Playwright工具包中的两种不同工具,自主完成了任务。

在这里,使用的Agent类型STRUCTUREDCHATZEROSHOTREACT_DESCRIPTION。要使用的工具则是PlayWrightBrowserToolkit,这是LangChain中基于PlayWrightBrowser包封装的工具箱,它继承自 BaseToolkit类。

PlayWrightBrowserToolkit 为 PlayWright 浏览器提供了一系列交互的工具,可以在同步或异步模式下操作。

其中具体的工具就包括:

https://static001.geekbang.org/resource/image/ce/46/ce51ayya392733c6b55ec3568caaac46.jpg?wh=1666x725

下面,我们就来看看结构化工具对话代理是怎样通过组合调用PlayWrightBrowserToolkit中的各种工具,自动完成我们交给它的任务。

from langchain.agents.agent_toolkits import PlayWrightBrowserToolkit
from langchain.tools.playwright.utils import create_async_playwright_browser

async_browser = create_async_playwright_browser()
toolkit = PlayWrightBrowserToolkit.from_browser(async_browser=async_browser)
tools = toolkit.get_tools()
print(tools)

from langchain.agents import initialize_agent, AgentType
from langchain.chat_models import ChatAnthropic, ChatOpenAI

# LLM不稳定,对于这个任务,可能要多跑几次才能得到正确结果
llm = ChatOpenAI(temperature=0.5)  

agent_chain = initialize_agent(
    tools,
    llm,
    agent=AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION,
    verbose=True,
)

async def main():
    response = await agent_chain.arun("What are the headers on python.langchain.com?")
    print(response)

import asyncio
loop = asyncio.get_event_loop()
loop.run_until_complete(main())

在这个示例中,我们询问大模型,网页python.langchain.com中有哪些标题目录?

很明显,大模型不可能包含这个网页的内部信息,因此,大模型不可避免地需要通过PlayWrightBrowser工具来解决问题。

第一轮思考

代理进入AgentExecutor Chain之后的第一轮思考如下:

https://static001.geekbang.org/resource/image/6a/02/6a5718eef084ac988a23e5488e967302.jpg?wh=1465x162

这里,我对上述思考做一个具体说明。

I can use the "navigatebrowser" tool to visit the website and then use the "getelements" tool to retrieve the headers. Let me do that.

这是第一轮思考,大模型知道自己没有相关信息,决定使用PlayWrightBrowserToolkit工具箱中的 navigate_browser 工具。

Action:{"action": "navigate_browser", "action_input": {"url": "[python.langchain.com"}}](link.juejin.cn/?target=htt…)

行动:通过Playwright浏览器访问这个网站。

Observation: Navigating to python.langchain.com returned status code 200

观察:成功得到浏览器访问的返回结果。

在第一轮思考过程中,模型决定使用PlayWrightBrowserToolkit中的navigate_browser工具。

第二轮思考

下面是大模型的第二轮思考。

https://static001.geekbang.org/resource/image/66/e5/663de1fda23de782af9233328ca5c2e5.jpg?wh=1473x298

还是对上述思考做一个具体说明。

Thought:Now that I have successfully navigated to the website, I can use the "get_elements" tool to retrieve the headers. I will specify the CSS selector for the headers and retrieve their text.

第二轮思考:模型决定使用PlayWrightBrowserToolkit工具箱中的另一个工具 get_elements,并且指定CSS selector只拿标题的文字。

Action: {"action": "get_elements", "action_input": {"selector": "h1, h2, h3, h4, h5, h6", "attributes": ["innerText"]}}

行动:用Playwright的 get_elements 工具去拿网页中各级标题的文字。

Observation: [{"innerText": "Introduction"}, {"innerText": "Get started​"}, {"innerText": "Modules​"}, {"innerText": "Model I/O​"}, {"innerText": "Data connection​"}, {"innerText": "Chains​"}, {"innerText": "Agents​"}, {"innerText": "Memory​"}, {"innerText": "Callbacks​"}, {"innerText": "Examples, ecosystem, and resources​"}, {"innerText": "Use cases​"}, {"innerText": "Guides​"}, {"innerText": "Ecosystem​"}, {"innerText": "Additional resources​"}, {"innerText": "Support"}, {"innerText": "API reference​"}]

观察:成功地拿到了标题文本。

在第二轮思考过程中,模型决定使用PlayWrightBrowserToolkit中的get_elements工具。

第三轮思考

下面是大模型的第三轮思考。

https://static001.geekbang.org/resource/image/01/6d/01e427d582973da438c67940f132166d.jpg?wh=1456x906

对上述思考做一个具体说明。

Thought:The headers on python.langchain.com are:

  1. Introduction … …
  2. API reference

第三轮思考:模型已经找到了网页中的所有标题。

Action:

json
复制代码
AI练中学
{
  "action": "Final Answer",
  "action_input": "The headers on python.langchain.com are: 1. Introduction 2. Get started 3. Modules 4. Model I/O 5. Data connection 6. Chains 7. Agents 8. Memory 9. Callbacks 10. Examples, ecosystem, and resources 11. Use cases 12. Guides 13. Ecosystem 14. Additional resources 15. Support 16. API reference"
}

行动:给出最终答案。

AgentExecutor Chain结束之后,成功输出python.langchain.com这个页面中各级标题的具体内容。

https://static001.geekbang.org/resource/image/96/63/961a5c0cc2b9c19d7147b2120608a663.jpg?wh=1462x126

在这个过程中,结构化工具代理组合调用了Playwright工具包中的两种不同工具,自主完成了任务。

Self-Ask with Search(自主询问搜索)代理、

Self-Ask with Search 也是LangChain中的一个有用的代理类型(SELFASKWITH_SEARCH)。它利用一种叫做 “Follow-up Question(追问)”加“Intermediate Answer(中间答案)”的技巧,来辅助大模型寻找事实性问题的过渡性答案,从而引出最终答案。

在这个示例中,我们使用SerpAPIWrapper作为工具,用OpenAI作为语言模型,创建Self-Ask with Search代理。

from langchain import OpenAI, SerpAPIWrapper 
from langchain.agents import initialize_agent, Tool
from langchain.agents import AgentType

llm = OpenAI(temperature=0)
search = SerpAPIWrapper()
tools = [
    Tool(
        name="Intermediate Answer", 
        func=search.run,
        description="useful for when you need to ask with search",
    )
]

self_ask_with_search = initialize_agent(
    tools, llm, agent=AgentType.SELF_ASK_WITH_SEARCH, verbose=True
)
self_ask_with_search.run(
    "使用玫瑰作为国花的国家的首都是哪里?"  
)

该代理对于这个问题的输出如下:

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/ab25c421ffce4f8fba795e1b8ca8985f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bCP5p2o5ZCM5a2mNzY2:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjEzNzU0MDc2NzM4MDMwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1732587079&x-orig-sign=p9DZh0nwXAnh5w6hGUWofO38p4k%3D

其实,细心的你可能会发现,“使用玫瑰作为国花的国家的首都是哪里? ”这个问题不是一个简单的问题,它其实是一个多跳问题——在问题和最终答案之间,存在中间过程

多跳问题(Multi-hop question)是指为了得到最终答案,需要进行多步推理或多次查询。 这种问题不能直接通过单一的查询或信息源得到答案,而是需要跨越多个信息点,或者从多个数据来源进行组合和整合。

也就是说,问题的答案依赖于另一个子问题的答案,这个子问题的答案可能又依赖于另一个问题的答案。这就像是一连串的问题跳跃,对于人类来说,解答这类问题可能需要从不同的信息源中寻找一系列中间答案,然后结合这些中间答案得出最终结论。

“使用玫瑰作为国花的国家的首都是哪里?”这个问题并不直接询问哪个国家使用玫瑰作为国花,也不是直接询问英国的首都是什么。而是先要推知使用玫瑰作为国花的国家(英国)之后,进一步询问这个国家的首都。这就需要多跳查询。

为什么 Self-Ask with Search 代理适合解决多跳问题呢?有下面几个原因。

  1. 工具集合:代理包含解决问题所必须的搜索工具,可以用来查询和验证多个信息点。这里我们在程序中为代理武装了SerpAPIWrapper工具。
  2. 逐步逼近:代理可以根据第一个问题的答案,提出进一步的问题,直到得到最终答案。这种逐步逼近的方式可以确保答案的准确性。
  3. 自我提问与搜索:代理可以自己提问并搜索答案。例如,首先确定哪个国家使用玫瑰作为国花,然后确定该国家的首都是什么。
  4. 决策链:代理通过一个决策链来执行任务,使其可以跟踪和处理复杂的多跳问题,这对于解决需要多步推理的问题尤为重要。

在上面的例子中,通过大模型的两次follow-up追问,搜索工具给出了两个中间答案,最后给出了问题的最终答案——伦敦。

Plan and execute(计划与执行) 代理。

计划和执行代理通过首先计划要做什么,然后执行子任务来实现目标。这个想法是受到 Plan-and-Solve 论文的启发。论文中提出了计划与解决(Plan-and-Solve)提示。它由两部分组成:首先,制定一个计划,并将整个任务划分为更小的子任务;然后按照该计划执行子任务。

这种代理的独特之处在于,它的计划和执行不再是由同一个代理所完成,而是:

  • 计划由一个大语言模型代理(负责推理)完成。
  • 执行由另一个大语言模型代理(负责调用工具)完成。

创建了Plan and execute代理,这个代理和之前看到的代理不同,它有一个Planner,有一个Executor,它们可以是不同的模型。

当然,在这个示例中,我们都使用了ChatOpenAI模型。

from langchain.chat_models import ChatOpenAI
from langchain_experimental.plan_and_execute import PlanAndExecute, load_agent_executor, load_chat_planner
from langchain.llms import OpenAI
from langchain import SerpAPIWrapper
from langchain.agents.tools import Tool
from langchain import LLMMathChain

search = SerpAPIWrapper()
llm = OpenAI(temperature=0)
llm_math_chain = LLMMathChain.from_llm(llm=llm, verbose=True)
tools = [
    Tool(
        name = "Search",
        func=search.run,
        description="useful for when you need to answer questions about current events"
    ),
    Tool(
        name="Calculator",
        func=llm_math_chain.run,
        description="useful for when you need to answer questions about math"
    ),
]
model = ChatOpenAI(temperature=0)
planner = load_chat_planner(model)
executor = load_agent_executor(model, tools, verbose=True)
agent = PlanAndExecute(planner=planner, executor=executor, verbose=True)

agent.run("在纽约,100美元能买几束玫瑰?")

输出如下:

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/6b778e6646da4a1aa435d296b9bd6186~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bCP5p2o5ZCM5a2mNzY2:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjEzNzU0MDc2NzM4MDMwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1732587114&x-orig-sign=py6sY2fYBgqoA0hcmPj9a71ogTk%3D

总结时刻

LangChain中常见的代理类型和它们的介绍。

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/75ee4b0ad1574f97bb8b50c4c6534486~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bCP5p2o5ZCM5a2mNzY2:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjEzNzU0MDc2NzM4MDMwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1732587185&x-orig-sign=X7YLhFbluk%2FYmH%2FRsD3hI4VONxg%3D