第13章 集成代理:Langchain和Transformers

本章讲述Gradio如何集成代理,包括两种智能体:LangChain Agent和Transformers Agent,先概述两者的概念和构建流程后,再介绍如何集成到Gradio应用。因为Agent作为连接各大模型和工具的桥梁,是打通人工通用智能(AGI)的最后一公里,对从业者和人工智能行业都是至关重要的一环,所以本章重点放在智能体的原理及应用步骤拆解,最后引入Smolagents示例作为对比。

13.1 LangChain Agent概念及AgentExecutor

本节介绍Agent相关概念,包括LangChain Agent与LangGraph区别、传统代理概念AgentExecutor及流行架构ReAct代理。Agent可译作智能体或代理,一般所指为智能体,本文会根据场景交替使用。

LangChain Agent与LangGraph区别。LangChain Agent是一种智能系统,它接受高级任务,并使用大型语言模型(LLM)作为推理引擎,由LLM决定采取哪些操作以及所需的输入;在通过工具调用(Tool-calling)执行操作后,结果将反馈给LLM,以判断是否需要执行更多操作或者结束任务。更多信息请参考🖇️链接13-1

LangGraph是LangChain的一个扩展,专门用于创建高度可控和可定制的Agent。目前官方建议使用LangGraph来构建Agent,这里限于篇幅不便扩展。LangGraph的更多信息请参考以下资源:

  • LangGraph docs on common Agent architectures🖇️链接13-2
  • Pre-built agents in LangGraph🖇️链接13-3

传统代理概念:AgentExecutor。LangChain之前引入了AgentExecutor作为代理的运行环境,虽然它是一个很好的起点,但在处理更复杂和定制化的代理时,灵活性显得不足,局限性逐渐显现。因此官方正在逐步淘汰AgentExecutor转而采用LangGraph,它是一个灵活且高度可控的运行环境。

但目前Gradio官方资料仍采用AgentExecutor,为了让读者更全面了解代理概念,仍对其原理及用法进行详细讲解。AgentExecutor更多信息请参阅:

  • 操作指南:Build an Agent with AgentExecutor (Legacy)🖇️链接13-4
  • 从AgentExecutor过渡到LangGraph:How to migrate from legacy LangChain agents to LangGraph🖇️链接13-5

流行架构:ReAct代理。构建Agent的一种流行架构是ReAct,它将推理(Reasoning)和行动(Acting)结合在一个迭代过程中——事实上,“ReAct”这个名字就代表了“推理”和“行动”,其基本流程如下:
(1)模型在接收到输入或之前的观察结果后,会“思考”应该采取什么步骤。
(2)模型或直接响应用户,或从可用工具中选择一个并为该工具生成参数。
(3)当选择工具时,代理运行环境(执行器)会解析出工具,并使用参数调用它。
(4)执行器会将工具执行的结果作为观察结果返回给模型。
(5)这个过程会重复进行,直到代理选择响应为止。

有一些基于通用提示的实现不需要模型任何特定功能,但最可靠的实现通常使用模型的工具调用(tool calling)等功能,以确保输出格式的可靠性并减少变异性。

13.2 AgentExecutor的构建流程

本节构建一个可以与搜索引擎交互的代理,用户能够向它提问,观察它调用搜索工具并与之对话。下面将创建工具、使用大语言模型激发工具、创建Agent并执行工具调用、添加记忆功能等方面使用AgentExecutor构建一个完整的代理。

13.2.1 构建步骤与IDE选择

准备工作包括构建AgentExecutor的步骤,并比较Google Colab与国内的在线平台阿里云天池实验室、华为云ModelArts‌。
构建步骤。构建与搜索引擎交互的代理的步骤如下所述:

  • 创建搜索工具(Tool),方便在线查找信息。
  • 创建检索器(Retriever),以便向代理提供特定信息。
  • 使用语言模型(LLM),特别是它们的工具调用能力。
  • 创建并执行代理(Agent),包括选择引导提示词。
  • 聊天历史记录(Chat History),使聊天机器人能够“记住”过去的交互,并在回答后续问题时考虑这些信息。
  • 使用LangSmith进行调试和追踪,以便优化应用程序。

开始之前请安装LangChain、设置LangSmith、选择合适的IDE。国外较出名的在线运行平台是Google Colab,而国内较著名的在线运行平台有阿里云天池实验室(🖇️链接13-6)及华为云ModelArts‌(🖇️链接13-7)。作者经过试用发现并不尽如人意,除了繁琐的云实名认证和授权配置,两者运行环境的默认Python版本较低且无法升级,与LangChain的许多子库(如langchain-community)无法兼容,或许自己创建虚拟环境才能使用LangChain的完整功能,与Google Colab打开即用的实用风格还有很大差距,不过喜欢探索的读者可以尝试。

13.2.2 创建工具——在线搜索器Tavily和检索器Retriever

首先需要创建要使用的工具,这里将使用两个工具:在线搜索引擎Tavily和基于本地索引构建的检索器Retriever。

Tavily在线搜索。LangChain内置了可直接使用Tavily搜索引擎的工具,可免费申请API密钥(🖇️链接13-8)。Tavily还提供免费服务层级,所以可以暂时不申请API密钥。获取并设置API密钥后,需定义并激发搜索工具,如代码13-1所示:

代码13-1
import getpass
import os
from langchain_community.tools.tavily_search import TavilySearchResults

os.environ["TAVILY_API_KEY"] = getpass.getpass("Please input TAVILY_API_KEY:")
search_tool = TavilySearchResults(max_results=2)
search_tool.invoke("what is the weather in SF?")

-> [{'title': 'Saturday, March 29, 2025. San Francisco, CA - Weather Forecast',
  'url': 'https://weathershogun.com/weather/usa/ca/san-francisco/480/march/2025-03-29',
  'content': 'San Francisco, California Weather: Saturday, March 29, 2025. Cloudy weather, overcast skies with clouds. Day 59°. Night 50°.',
  'score': 0.95598555}, 
 {'title': ...}]

关于Tavily搜索工具的更详细用法请参阅:Tavily Search🖇️链接13-9。此外,还可以在Tavily官网在线测试运行结果:Tavily Playground🖇️链接13-10

除了langchain_community,还可使用langchain-tavily,如代码13-2所示:

代码13-2
#!pip install -qU langchain-tavily
from langchain_tavily import TavilySearch
search_tool = TavilySearch(max_results=2, topic="general")

Retriever检索器。对搜索到的数据创建一个检索器RAG(检索增强生成),有关创建RAG步骤的深入解释,请参阅:Build a Retrieval Augmented Generation (RAG) App🖇️链接13-11。创建检索器如代码13-3所示:

代码13-3
#!pip install -qU faiss-cpu langchain-community langchain-huggingface
from langchain_community.document_loaders import WebBaseLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
loader = WebBaseLoader("https://docs.smith.langchain.com/overview")
docs = loader.load()
documents = RecursiveCharacterTextSplitter(
    chunk_size=1000, chunk_overlap=200
).split_documents(docs)

import faiss
from langchain_community.vectorstores import FAISS
from langchain_community.docstore.in_memory import InMemoryDocstore
from langchain_huggingface.embeddings import HuggingFaceEmbeddings
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-mpnet-base-v2")
embedding_dim = len(embeddings.embed_query("hello world"))
index = faiss.IndexFlatL2(embedding_dim)

vector = FAISS.from_documents(documents, embeddings)
retriever = vector.as_retriever()
retriever.invoke("how to upload a dataset")[0]

这段代码实现了一个完整的RAG管道。首先文档加载与分块,WebBaseLoader从URL抓取网页内容,自动提取正文文本;RecursiveCharacterTextSplitter递归分割文本,优先按段落、句子、单词的层级切分,每块1000字符,重叠200字符,保持相邻块之间的上下文连续性。然后,创建向量存储组件,初始化嵌入模型,获取嵌入维度(768维的高质量嵌入模型),创建FAISS索引(L2欧氏距离)。最后,构建检索器,从文档创建向量数据库,创建检索器并执行检索查询。完整回复内容如下所示:

Document(id='bdae82ba-a15a-4438-8f7d-fbbebd5eed41', metadata={'source': 'https://docs.smith.langchain.com/overview', 'title': 'Get started with LangSmith | \uf8ffü¶úÔ∏è\uf8ffüõ†Ô∏è LangSmith', 'description': 'LangSmith is a platform for building production-grade LLM applications.', 'language': 'en'}, page_content='Get started by adding tracing to your application. ...')

现在已经创建具有索引数据的检索器,然后利用函数create_retriever_tool将其转换为检索器工具(代理正确使用它所需的格式),最后根据工具search_tool和retriever_tool,创建一个将在下游使用的工具列表,如代码13-4所示:

代码13-4
from langchain.tools.retriever import create_retriever_tool
retriever_tool = create_retriever_tool(retriever, "langsmith_search",
    "Search for information about LangSmith. For any questions about LangSmith, you must use this tool!")
tools = [search_tool , retriever_tool]

13.2.3 使用大模型DeepSeek激发工具

创建大模型并执行激发。LangChain支持多种大模型,例如DeepSeek、OpenAI、Anthropic、Azure、Groq、Mistral AI及xAI等,以DeepSeek为例创建大模型对象,并传入消息列表来调用大语言模型,默认返回的是内容字符串如代码13-5所示:

代码13-5
# pip install -Uq langchain langchain-core
import getpass
import os
from langchain_deepseek import ChatDeepSeek

if not os.environ.get("DEEPSEEK_API_KEY"):
  os.environ["DEEPSEEK_API_KEY"] = getpass.getpass("Enter API key for DeepSeek: ")
model = ChatDeepSeek(model="deepseek-chat", temperature=0,
    max_tokens=None, timeout=None, max_retries=2)

from langchain_core.messages import HumanMessage
response = model.invoke([HumanMessage(content="hi!")])
response.content
->Hello! How can I assist you today? 😊

绑定和使用工具。使用bind_tools()方法让大模型知晓这些工具的存在,然后先用普通消息进行调用并观察响应的content字段和tool_calls字段,如代码13-6所示:

代码13-6
model_with_tools = model.bind_tools(tools)
query = "Hi!"
response = model_with_tools.invoke([{"role": "user", "content": query}])
print(f"Message content: {response.text()}\n")
print(f"Tool calls: {response.tool_calls}")

->Message content: Hello! I'm here to help you...
Tool calls: []

可以看到模型无工具调用。尝试输入预期会触发工具调用的内容:

query = "Search for the weather in SF"
response = model_with_tools.invoke([{"role": "user", "content": query}])
print(f"Message content: {response.text()}\n")
print(f"Tool calls: {response.tool_calls}")

->Message content: I'll help you search for information about the weather in SF.
Tool calls: [{'name': 'tavily_search', 'args': {'query': 'current weather San Francisco'}, 'id': 'toolu_015gdPn1jbB2Z21DmN2RAnti', 'type': 'tool_call'}]

此时可以看到返回结果出现了tool_calls字段!这表明大模型希望调用Tavily搜索工具。注意:目前这仅是工具调用指令,尚未实际执行调用。要实现真正的工具调用,就需要创建调用工具的智能体代理(Agent)。

13.2.4 导入提示词、创建Agent及AgentExecutor

在完成工具和大语言模型的定义后,可以创建Agent实际执行工具调用。
选择提示词:prompt。首先选定用于指导智能体行为的提示词模板,如需查看该提示词的具体内容(需LangSmith访问权限),可访问LangChain Hub🖇️链接13-12。比如本例使用的hwchase17/openai-functions-agent,如图13-1所示:
在这里插入图片描述

图13-1

导入提示词,如代码13-7所示:

代码13-7
from langchain import hub
prompt = hub.pull("hwchase17/openai-functions-agent")
prompt.messages

->[SystemMessagePromptTemplate(prompt=PromptTemplate(input_variables=[], template='You are a helpful assistant')),
 MessagesPlaceholder(variable_name='chat_history', optional=True),
 HumanMessagePromptTemplate(prompt=PromptTemplate(input_variables=['input'], template='{input}')),
 MessagesPlaceholder(variable_name='agent_scratchpad')]

创建代理和AgentExecutor。在大模型的回复中得到工具调用指令后,需要代理执行具体的工具调用,这是工具调用型智能体所完成的功能。通过以下要素初始化智能体:大语言模型(LLM)、工具集和提示词模板,它的核心职责是接收输入并决策需要执行的动作,但不负责实际执行——该功能由后续的AgentExecutor实现。注意传入的是基础模型而非工具绑定版模型,因为create_tool_calling_agent()会在底层自动调用bind_tools()。将智能体(决策中枢)与工具集通过AgentExecutor整合,它将循环调用智能体获取决策、实际执行工具调用及管理整个工作流程的运转。如代码13-8所示:

代码13-8
from langchain.agents import create_tool_calling_agent, AgentExecutor
agent = create_tool_calling_agent(model, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools)

13.2.5 测试代理的工具调用

现在用几个查询来测试智能体了。请注意,当前测试的都是无状态查询,系统不会记住之前的交互记录。

  • 测试1:无需调用工具的情况。首先观察智能体在不需要调用工具时的响应行为:
agent_executor.invoke({"input": "hi!"})

->{'input': 'hi!', 'output': 'Hello! How can I assist you today? 😊'}

验证:可通过查看LangSmith跟踪记录,确认确实没有触发任何工具调用。

  • 测试2:触发检索器调用。测试需要调用本地检索器的场景:
agent_executor.invoke({"input": "how can langsmith help with testing?"})

->{'input': 'how can langsmith help with testing?',
 'output': 'LangSmith is a platform that aids in building production-grade Language Learning Model (LLM) applications. ...'}

验证:检查LangSmith跟踪日志,通过检索query生成情况及返回结果的处理流程,确认检索器被正确调用。

  • 测试3:触发搜索引擎调用。测试需要调用Tavily搜索引擎的场景:
agent_executor.invoke({"input": "whats the weather in beijing?"})

->{'input': 'whats the weather in beijing?',
 'output': 'The current weather in Beijing is sunny with a temperature of 11.3°C (52.3°F)...\n\nFor more details, you can check [Weather API](https://www.weatherapi.com/) or [Weather Underground](https://www.wunderground.com/weather/cn/beijing).'}

验证:通过LangSmith跟踪确认搜索关键词的生成逻辑、API调用的耗时统计及搜索结果摘要的生成质量。由于LangSmith需要收费,所以使用官方的LangSmith跟踪界面进行展示(可能与本示例有细节出入,但不影响验证结果,只需将ChatOpenAI看作ChatDeepSeek即可),如图13-2所示:
在这里插入图片描述

图13-2

13.2.6 记录对话历史与自动记忆管理

如前所述,当前智能体是无状态的,这意味着它不会记住之前的交互记录。要为其添加记忆能力,需要传入历史对话记录(chat_history)。

记录对话历史:chat_history。注意,由于当前使用的提示词模板hwchase17/openai-functions-agent的硬性要求,历史记录变量必须命名为chat_history,如果使用不同的提示词模板,可以更改这个变量名。
首先,通过函数invoke在第一个消息中传入空列表chat_history;然后,将第一步回复的信息插入到chat_history,用户输入使用消息类型HumanMessage,而大模型的回复使用消息类型AIMessage,如代码13-9所示:

代码13-9
agent_executor.invoke({"input": "hi! my name is bob", "chat_history": []})

from langchain_core.messages import AIMessage, HumanMessage
agent_executor.invoke(
    {
        "chat_history": [
            HumanMessage(content="hi! my name is bob"),
            AIMessage(content="Hi, Bob! How can I assist you today?"),
        ],
        "input": "what's my name?",
    }
)

-> {'chat_history': [HumanMessage(content='hi! my name is bob', additional_kwargs={}, response_metadata={}),
  AIMessage(content='Hi, Bob! How can I assist you today?', additional_kwargs={}, response_metadata={})],
 'input': "what's my name?",
 'output': 'Your name is Bob! You introduced yourself earlier. 😊 How can I help you, Bob?'}

自动记忆管理:RunnableWithMessageHistory。实现对话历史的自动跟踪,由于涉及多个输入源,需要明确指定两个关键参数:①input_messages_key:标识要加入对话历史的输入键名;②history_messages_key:标识加载历史消息的目标键名。

最后,定义并调用RunnableWithMessageHistory封装器,需配置session_id,用法如代码13-10所示:

代码13-10
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
store = {}
def get_session_history(session_id: str) -> BaseChatMessageHistory:
    if session_id not in store:
        store[session_id] = ChatMessageHistory()
    return store[session_id]

agent_with_chat_history = RunnableWithMessageHistory(
    agent_executor,
    get_session_history,
    input_messages_key="input",
    history_messages_key="chat_history",
)
agent_with_chat_history.invoke(
    {"input": "hi! I'm bob"},
    config={"configurable": {"session_id": "<foo>"}},
)

->{'input': "hi! I'm bob",
 'chat_history': [],
 'output': 'Hi, Bob! How can I assist you today? 😊'}

再次调用时传入session_id,代理会将其识别为同一会话,如代码13-11所示:

代码13-11
agent_with_chat_history.invoke(
    {"input": "what's my name?"},
    config={"configurable": {"session_id": "<foo>"}},
)

 {'input': "what's my name?",
 'chat_history': [HumanMessage(content="hi! I'm bob", additional_kwargs={}, response_metadata={}),
  AIMessage(content='Hi, Bob! How can I assist you today? 😊', additional_kwargs={}, response_metadata={})],
 'output': 'Your name is Bob.'}

至此构建了一个完整的AgentExecutor!本次快速入门教程已完整演示了如何创建一个基础Agent,Agent是一个复杂的主题,仍有大量知识值得深入探索!重要提示:本教程介绍的是使用LangChain原生智能体的构建方法,虽然适合入门学习,但当需求发展到一定复杂度时,可能会需要更大的灵活性和控制力,要开发更高级的智能体,推荐转向LangGraph框架。

13.2.7 练习11:AgentExecutor访问搜索引擎

大语言模型本身无法执行操作——它们只能输出文本。LangChain的一个重要应用场景是创建代理,LLM能够自主决定某任务采取哪些行动,然后代理决定具体执行并返回结果,LLM观察结果决定下一步行动,重复此过程直到任务完成。而LangChain Agent集成了LLM,它本身可以独立完成任务,可以使用它访问搜索引擎。

**创建AgentExecutor。**从导入库和设置Langchain Agent开始,设置秘钥SERPAPI_API_KEY、HF_TOKEN和DEEPSEEK_API_KEY三个API秘钥为环境变量,并创建模型、Agent及AgentExecutor,如代码13-12所示:

代码13-12
# !pip install -Uq langchain-community langchain-deepseek gradio google-search-results
from langchain import hub
from langchain.agents import AgentExecutor, create_openai_tools_agent, load_tools
from langchain_deepseek import ChatDeepSeek
from gradio import ChatMessage
import gradio as gr
import os

if not (os.getenv("SERPAPI_API_KEY") and os.getenv("DEEPSEEK_API_KEY")):
    with gr.Blocks() as demo:
        gr.Markdown("""
# Chat with a LangChain Agent 🦜⛓ and see its thoughts 💭
In order to run this space, duplicate it and add the following space secrets:
* SERPAPI_API_KEY - create an account at serpapi.com and get an API key
* DEEPSEEK_API_KEY - create an deepseek account and get an API key
""")
    demo.launch()

model = ChatDeepSeek(model="deepseek-chat", temperature=0,
    max_tokens=None, timeout=None, max_retries=2)
tools = load_tools(["serpapi"])
prompt = hub.pull("hwchase17/openai-tools-agent")
agent = create_openai_tools_agent(
    model.with_config({"tags": ["agent_llm"]}), tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools).with_config(
    {"run_name": "Agent"})

代码构建了一个基于DeepSeek模型的AI工具代理系统,主要功能如下:

  • 检查API秘钥:当SERPAPI_API_KEY或DEEPSEEK_API_KEY为空时,输出提示。
  • 模型初始化‌:使用ChatDeepSeek创建了一个名为"deepseek-chat"的模型实例,参数设置为确定性输出,无最大token限制和超时限制,并最多重试2次。
  • 工具加载‌:通过load_tools加载了SerpAPI工具(一个搜索引擎API工具),使代理能够执行网络搜索。‌
  • 提示词获取‌:从LangChain Hub获取"hwchase17/openai-tools-agent"预定义提示模板,用于指导代理的行为。‌
  • 代理创建‌:将模型、工具和提示词组合,创建了一个具备工具使用能力的AI代理,并为模型添加了"agent_llm"标签。‌
  • 执行器配置‌:最终创建了一个AgentExecutor实例来运行这个代理和执行工具,并为执行器设置了"Agent"的运行名称。

创建Gradio UI。如代码13-13所示:

代码13-13
async def interact_with_langchain_agent(prompt, messages):
    messages.append(ChatMessage(role="user", content=prompt))
    yield messages
    async for chunk in agent_executor.astream({"input": prompt}):
        if "steps" in chunk:
            for step in chunk["steps"]:
                messages.append(ChatMessage(role="assistant", content=step.action.log,
                    metadata={"title": f"🛠 Used tool {step.action.tool}"}))
                yield messages
        if "output" in chunk:
            messages.append(ChatMessage(role="assistant", content=chunk["output"]))
            yield messages

with gr.Blocks() as demo:
    gr.Markdown("# Chat with a LangChain Agent 🦜⛓ and see its thoughts 💭")
    chatbot_2 = gr.Chatbot(label="Agent",
        avatar_images=(None,
            "https://em-content.zobj.net/source/twitter/141/parrot_1f99c.png"))
    input_2 = gr.Textbox(lines=1, label="Chat Message")
    input_2.submit(interact_with_langchain_agent, [input_2, chatbot_2], [chatbot_2])
demo.launch(debug=True)

这段代码展示了如何创建一个基于LangChain的交互式聊天代理界面,使用Gradio作为前端框架。以下是详细解析:

  • 核心功能函数:interact_with_langchain_agent。此异步函数处理用户与LangChain代理的交互流程:‌①消息记录‌,将用户输入添加到消息历史中。‌②流式处理‌,通过agent_executor.astream异步流式处理用户输入。③工具使用追踪‌,当代理使用工具时,记录工具使用步骤‌。④输出处理‌,捕获并返回代理的最终输出。
  • Gradio界面配置。界面包含以下核心组件:①标题‌,使用gr.Markdown显示。②‌聊天机器人‌,配置gr.Chatbot的消息格式、标签及聊天头像。③‌输入框‌:gr.Textbox用于接收用户输入‌。④交互绑定‌:将输入框的submit事件绑定interact_with_langchain_agent函数。

运行Gradio。安装包:langchain-community、langchain_openai、serpapi、google-search-results、gradio后重启会话,然后设置DEEPSEEK_API_KEY和SERPAPI_API_KEY。未设置API时出现提示信息,设置API后运行界面如图13-3所示:
在这里插入图片描述

图13-3

读者也可以在Spaces查看完整演示:gradio/langchain-agent🖇️链接13-13

13.3 Transformers Agent概念、分类和ReAct

gr.Chatbot原生支持显示中间思考过程和工具使用情况(参考其参数metadata用法),这使得它非常适合为LLM Agent、思维链(Chain-of-Thought, CoT)或推理演示创建用户界面。下面将展示如何使用gr.Chatbot和gr.ChatInterface来显示Transformers Agent的思考过程和工具使用情况,详细资料请参考:License to Call: Introducing Transformers Agents 2.0🖇️链接13-14

在开始之前,请确保先阅读并理解smolagents🖇️链接13-15。随着LLM Agent的用途越来越重要,Transformers实现了Agent,但随着智能体概念爆火,库agents已从Transformers独立,成为现在的smolagents。但本书还是以前例为主,这两个库的API非常相似,切换很容易。

Transformers Agent概念。经过大量因果语言建模(causal language modeling)训练的大语言模型能处理多种任务,但在逻辑、计算和搜索等基本任务上往往表现不佳,通常无法生成期望答案,克服这一弱点的方法是创建LLM Agent。

LLM Agent的定义非常宽泛:通常指的是所有将LLM作为核心引擎,并能够根据观察对其环境施加影响的系统。这些系统能够通过多次迭代“感知 ⇒ 思考 ⇒ 行动”的循环来实现既定任务,并常常融入规划或知识管理系统以提升其表现效能。并且LLM Agent可以访问工具(Tools),这些工具通常是用于执行任务的函数,必须包含代理正确使用所需的描述。对智能体领域的精彩评述论文:The Rise and Potential of Large Language Model Based Agents: A Survey(🖇️链接13-16)。

Agent分类。LLM Agent可以设计为一系列动作/工具(actions/tools)并一次性运行它们,也可以逐个计划后执行动作/工具,并在启动下一个动作之前等待前面每个动作的结果。因此根据代理设计理念的不同,Transformers将代理分为两种类型:
(1)Code agent:该代理先有一个规划步骤,然后生成Python代码以一次性执行所有动作。它原生支持处理工具内不同的输入输出类型,因此是多模态任务的首选。
(2)React agent:它采用一种基于“推理 (Reasoning)”与“行动 (Acting)”结合的方式逐步解决给定任务,并以此来构建智能体。Transformers有三种版本:
①ReactAgent:原始的推理执行代理,它的行动将从LLM的输出中解析出来,它并不常用,经常会被下面两种衍生代理代替。
②ReactJsonAgent:工具调用将由LLM以JSON代码块生成,然后解析并执行。
③ReactCodeAgent:是一种新型的ReactJsonAgent,工具调用将由LLM以Python代码块生成,这对于具有强大编码性能的LLM非常有效。
单步Code agent与多步React agent执行流程区别如图13-4所示:
在这里插入图片描述

图13-4

详解ReAct智能体。它是解决推理任务的首选代理,在提示词中阐述了模型能够利用哪些工具,并引导它逐步思考“step by step” (即Chain-of-Thought,思维链),以规划并实施其后续动作,ReAct框架使代理可以在基于先前观察的基础上进行多次非常高效的思考。可以阅读论文:ReAct: Synergizing Reasoning and Acting in Language Models(🖇️链接13-17)以了解更多关于使用ReAct代理的信息。为方便理解ReAct流程,查看ReactCodeAgent解决问题的执行步骤,如代码13-14所示:

代码13-14
agent.run("How many more blocks (also denoted as layers) in BERT base encoder than the encoder from the architecture proposed in Attention is All You Need?")
=====New task=====
How many more blocks (also denoted as layers) in BERT base encoder than the encoder from the architecture proposed in Attention is All You Need?

====Agent is executing the code below:
bert_blocks = search(query="number of blocks in BERT base encoder")
print("BERT blocks:", bert_blocks)
====
Print outputs:
BERT blocks: twelve encoder blocks

====Agent is executing the code below:
attention_layer = search(query="number of layers in Attention is All You Need")
print("Attention layers:", attention_layer)
====
Print outputs:
Attention layers: Encoder: The encoder is composed of a stack of 6 identical layers.
 
====Agent is executing the code below:
bert_blocks = 12
attention_layers = 6
diff = bert_blocks - attention_layers
print("Difference in blocks:", diff)
final_answer(diff)
====
Print outputs:
Difference in blocks: 6
Final answer: 6

13.4 构建Transformers Agent参数详解

构建Agent的方法。需要设置transformers中各类Agent的构造参数:
(1)大模型引擎(llm_engine):此参数设置为代理提供动力引擎的LLM,代理并不完全是LLM,它更像是一个使用LLM作为引擎的程序。
(2)工具箱(tools):代理从中挑选工具来执行任务。
(3)系统提示(system prompt):LLM引擎将根据这个提示生成输出。
(4)工具解析器(tool_parser):用于从LLM的输出中提取需要调用的工具及其参数。

在Agent系统初始化时,工具的属性被用来生成工具描述,并将其嵌入到代理的system_prompt中,以便大模型引擎知道它可以使用哪些工具以及为什么使用这些工具。然后LLM根据系统提示输出解决方案,解析器解析LLM输出后,决定调用哪些工具继续执行。最后当得到答案或者满足停止条件后将结束会话,否则继续迭代执行。

本节讲解如何构建Transformers Agent,主要是定义其四个构造参数,还有一些其他设置,下面逐一讲述。开始之前,使用命令额外安装Transformers的agents,它会安装所有默认依赖项:pip install transformers[agents]。更详细信息请参考:Transformers - Agents & Tools🖇️链接13-18

13.4.1 四类大模型引擎

可以自由创建和使用智能体框架的引擎,但需满足以下条件:

  • 引擎遵循输入消息的消息格式(List[Dict[str, str]]),并返回一个字符串。
  • 引擎在传入的参数stop_sequences指定的序列处停止生成输出。

下面就来了解下四类创建引擎方法,请注意区分它们适用的场景。

模型引擎函数:llm_engine。可以通过定义一个接受消息列表并返回文本的函数llm_engine()来构建LLM引擎,还接受一个stop参数,如代码13-15所示:

代码13-15
from huggingface_hub import login, InferenceClient
from transformers import CodeAgent
login("<YOUR_HUGGINGFACEHUB_API_TOKEN>")
client = InferenceClient(model="meta-llama/Meta-Llama-3-70B-Instruct")
def llm_engine(messages, stop_sequences=["Task"]) -> str:
    response = client.chat_completion(messages, stop=stop_sequences, max_tokens=1000)
    answer = response.choices[0].message.content
    return answer
agent = CodeAgent(llm_engine=llm_engine)

此外,llm_engine()还可以接受一个grammar参数。如果在智能体初始化时指定了grammar,这个参数将连同初始化时定义的grammar一起传递给llm_engine的调用,以实现受约束的生成,从而强制智能体生成格式正确的输出。

TransformersEngine类。它将预初始化的Pipeline作为输入实现上述功能,或使用可选的model_id,以便使用transformers在本地运行推理。如代码13-16所示:

代码13-16
from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline, TransformersEngine
model_name = "HuggingFaceTB/SmolLM-135M-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name)
pipe = pipeline("text-generation", model=model, tokenizer=tokenizer)
engine = TransformersEngine(pipe)
engine([{"role": "user", "content": "Ok!"}], stop_sequences=["great"])
# 输出 "What a "

HfApiEngine类。由于智能体通常需要更强的模型,如Llama-3.1-70B-Instruct,这些模型目前较难在本地运行,因此有HfApiEngine类,它是封装了Hugging Face Inference API客户端的引擎,在底层初始化huggingface_hub.InferenceClient,用于执行大语言模型(LLM)的调用。该引擎通过Hugging Face的推理API与语言模型进行通信,它即可以在无服务器模式下使用,也可以与专用端点(dedicated endpoint)一起使用,并支持诸如停止序列和语法自定义等功能。程序中甚至可以留空llm_engine参数,默认情况下会创建一个HfApiEngine。用法如代码13-17所示:

代码13-17
from transformers import HfApiEngine
llm_engine = HfApiEngine(model="meta-llama/Meta-Llama-3-70B-Instruct")

HfEngine类。对于本地部署的具有Inference API的LLM,还可以直接使用包中提供的HfEngine类来获取,如代码13-18所示:

代码13-18
from transformers.agents import HfEngine
llm_engine = HfEngine("meta-llama/Meta-Llama-3-70B-Instruct")

13.4.2 工具箱与创建加载工具

工具是Agent使用的原子函数,例如PythonInterpreterTool,包括各种属性和执行方法。当Agent初始化时,工具的属性被用来生成工具描述,并将其嵌入到Agent的系统提示中,这让Agent知道它如何使用这些工具以及为什么使用。工具是Agent的核心部分,工具的好坏和数量决定了Agent能力的大小。

1. 默认工具箱与管理方法

默认工具箱(toolbox)。Transformers附带了一个默认工具箱,用于增强Agent的功能。构建Agent时需要一个tools参数,它接受一个工具列表(List[Tools]),这个列表可以为空。在Agent初始化时,可通过定义可选参数add_base_tools=True添加默认工具箱到工具列表。默认工具箱中工具如下:

  • 文档问答(document_question_answering):给定一个图像格式的文档(如PDF),回答关于该文档的问题(Donut)。
  • 图像问答(image_question_answering):给定图像,回答关于它的问题(VILT)。
  • 语音转文本(speech_to_text):给定一段人声录音,将语音转录为文本(Whisper)。
  • 文本转语音(text_to_speech):将文本转换为语音(SpeechT5)。
  • 翻译(translation):将给定的句子从源语言翻译为目标语言。

管理工具箱方法。当已经初始化带有工具箱的Agent时,从头重新初始化并添加工具会很不方便。此时可使用toolbox,通过add_tool()和update_tool()来添加或替换工具来管理代理的工具箱。比如通过add_tool()将model_download_tool()添加到一个仅使用默认工具箱初始化的已有代理中,如代码13-19所示:

代码13-19
from transformers import CodeAgent
agent = CodeAgent(tools=[], llm_engine=llm_engine, add_base_tools=True)
agent.toolbox.add_tool(model_download_tool)

现在就可以同时利用新工具和旧工具。注意:在为已经运行良好的Agent添加工具时要小心,因为它可能会偏向选择自定义的工具,或者选择与原工具不同的工具。

另外,还可以使用方法agent.toolbox.update_tool()替换工具箱中的现有工具,尤其是新工具一对一替换现有工具时将非常有用。由于Agent已经知道如何执行该特定任务,因此替换时只需确保新工具遵循与被替换工具相同的API,或者调整系统提示模板以确保更新所有使用被替换工具的示例。

2. 创建新工具、load_tool()与ToolCollection

创建新工具。可以为Hugging Face默认工具中未涵盖的用例创建自定义工具,以返回Hub上某类任务的下载量最多的模型为例,将核心代码封装为工具函数以快速将其转换为工具,操作时只需添加工具装饰器@tool即可,如代码13-20所示:

代码13-20
from transformers import tool
from huggingface_hub import list_models
@tool
def model_download_tool(task: str) -> str:
    """This is a tool that returns the most downloaded model of a given task on the Hugging Face Hub.
    It returns the name of the checkpoint.
    Args:
        task: The task for which"""
    model = next(iter(list_models(filter="text-classification", sort="downloads", direction=-1)))
    return model.id

代码封装为工具函数,此函数需要:

  • 清晰的函数名:名称通常描述工具的功能,通常要切合完成的具体任务。
  • 输入和输出的类型提示:输入类型提示为函数的入参,输出类型提示为符号->后的类型,方便大模型调用。
  • 函数描述:描述函数的作用和返回说明,其中包括一个“Args:”部分,描述每个参数的作用(注意这里不需要类型指示,因为会从参数中提取)。

所有这些信息都将在初始化时自动嵌入到系统提示中,因此请尽量清晰易懂!最后将工具直接添加到初始化代理的参数tools中。假设创建智能体CodeAgent并使用工具model_download_tool,如代码13-21所示:

代码13-21
from transformers import CodeAgent, HfApiEngine
llm_engine = HfApiEngine(model="meta-llama/Meta-Llama-3-70B-Instruct")
agent = CodeAgent(tools=[model_download_tool], llm_engine=llm_engine)
agent.run("Can you give me the name of the model that has the most downloads in the 'text-to-video' task on the Hugging Face Hub?")

======== New task ========
Can you give me the name of the model that has the most downloads in the 'text-to-video' task on the Hugging Face Hub?
==== Agent is executing the code below:
most_downloaded_model = model_download_tool(task="text-to-video")
print(f"The most downloaded model for the 'text-to-video' task is {most_downloaded_model}.")
==== The output: 
"The most downloaded model for the 'text-to-video' task is ByteDance/AnimateDiff-Lightning."

load_tool()函数加载工具。该函数手动加载模型为工具,如代码13-22所示:

代码13-22
from transformers import load_tool
tool = load_tool("text-to-speech")
audio = tool("This is a text to speech tool")

工具集合ToolCollection。还可以通过transformers中的对象ToolCollection来利用工具集合,并通过参数collection_slug指定想要使用的集合片,它们将作为列表传递给Agent进行初始化,如代码13-23所示:

代码13-23
from transformers import ToolCollection, ReactCodeAgent
image_tool_collection = ToolCollection(collection_slug="huggingface-tools/diffusion-tools-6630bb19a942c2306a2cdb6f")
agent = ReactCodeAgent(tools=[*image_tool_collection.tools], add_base_tools=True)
agent.run("Please draw me a picture of rivers and lakes.")

需要注意的是,为了加快启动速度,工具只有在被调用时才会加载。

13.4.3 系统提示和工具解析器

在Agent系统初始化时,工具的属性被用来生成工具描述,将其嵌入到智能体的system_prompt中,以便智能体知道它可以使用哪些工具以及工具用途,然后大模型根据系统提示输出解决方案,本小节将详解系统提示及工具解析器。

系统提示和工具解析器示例。系统提示(system prompt)和工具解析器(tool parser)是自动定义的,可以通过调用代理的system_prompt_template和tool_parser进行查看。Agent由LLM驱动,它会根据系统提示生成输出。在系统提示中,尽可能清楚地解释想要执行的任务非常重要,它还可以根据预期的任务进行定制和调整。由于Agent是由LLM驱动的,且每次run()操作都是独立的,提示中的微小变化可能会产生完全不同的结果,因此也可以连续运行Agent以执行不同的任务:每次运行时,agent.task和agent.logs属性都会重新初始化。

例如,查看ReactCodeAgent示例中的工具解析器和系统提示(输出有简化):

>>> print(agent.tool_parser)
<function parse_json_tool_call at 0x79c1edcddb20>
>>> print(agent.system_prompt_template)
You are an expert assistant who can solve any task using code blobs. You will be given a task to solve as best you can.
To do so, you have been given access to a list of tools: these tools are basically Python functions which you can call with code.
To solve the task, you must plan forward to proceed in a series of steps, in a cycle of 'Thought:', 'Code:', and 'Observation:' sequences.
...

工具解析器只列出了工具名称和内存地址。而观察系统提示示例,可以发现系统提示极为详细,一般包括:

  • 一段介绍,解释代理应如何行为以及工具是什么。
  • 所有工具的描述,这些描述由<<tool_descriptions>>标记定义,该标记在运行时动态替换为用户定义/选择的工具,工具描述来源于工具属性,包括名称、描述、输入和输出类型,以及一个用户可以优化的简单jinja2模板。
  • 示例examples,示例中尽量包含所提到的工具及使用方法。
  • 预期的输出格式,对final_answer的具体要求。

系统提示格式只是起到引导作用,并没有具体的格式规定,可根据自己需求添加或删除,但一般情况是:越具体越清晰的系统提示会产生更好的输出效果!

修改系统提示。可以修改系统提示,例如通过添加对输出格式的解释以强制产生需要的输出。为了获得最大的灵活性,甚至可以通过将自定义提示作为参数传递给system_prompt参数来覆盖整个系统提示模板。具体操作时,可通过agent.system_prompt_template获取初始化的系统提示,然后根据自己需要修改后进行替换,操作如代码13-24所示:

代码13-24
from transformers import ReactJsonAgent
from transformers.agents import PythonInterpreterTool
agent = ReactJsonAgent(tools=[PythonInterpreterTool()], system_prompt="{your_custom_prompt}")

请确保模板中某个位置定义<<tool_descriptions>>,以便代理获取工具信息。

13.4.4 运行库、运行参数和运行状况

除了引擎、工具、系统提示和解析器外,还有一些辅助设置有助于使用智能体。比如CodeAgent在执行生成代码时,可能需要导入某些运行库;或者Agent运行时需添加句子、文件等可替换参数;当代理运行完毕后查看代理的运行情况等。

导入运行库。Python解释器会在一组与工具一起传递的输入上执行代码,这理应是安全的,因为解释器只能调用传入的工具函数(特别是Hugging Face工具)和print函数,因此已经限制了可以执行的内容。Python解释器默认不允许在安全列表之外导入,因此所有显式攻击都不应该得逞。但仍然可以通过在初始化ReactCodeAgent或CodeAgent时,将授权模块作为字符串列表传递给additional_authorized_imports参数来授权额外的导入,如代码13-25所示:

代码13-25
from transformers import ReactCodeAgent
agent = ReactCodeAgent(tools=[], additional_authorized_imports=['requests', 'bs4'])
agent.run("Could you get me the title of the page at url 'https://huggingface.co/blog'?")
(...)
'Hugging Face – Blog'

python解释器会尝试在执行任何非法操作或存在常规错误的代码处停止。LLM可以生成任意代码后执行,但不要添加任何不安全的导入!
agent.run()的运行参数。智能体在底层是如何工作的?本质上,智能体的作用是“允许LLM使用工具”。智能体有一个关键的方法agent.run(),该方法:

  • 在一个特定提示中向LLM提供关于工具使用的信息。
  • 解析来自LLM输出的工具调用 (可通过代码、JSON或任何其他格式)并执行。
  • 如果智能体需要对先前的输出进行迭代,那么它会保留先前的工具调用和观察存储。这个存储可以根据持续的时间长短而变得更精炼或更细致。

当代理调用方法run()时,可以添加额外的参数,比如参数sentence可以将文本作为额外参数传递给模型,如代码13-26所示:

代码13-26
from transformers import CodeAgent, HfApiEngine
llm_engine = HfApiEngine(model="meta-llama/Meta-Llama-3-70B-Instruct")
agent = CodeAgent(tools=[], llm_engine=llm_engine, add_base_tools=True)
agent.run("Could you translate this sentence from French, say it out loud and return the audio.", sentence="Où est la boulangerie la plus proche?")

另外,可以使用audio参数来指示模型使用本地或远程文件,如代码13-27所示:

代码13-27
```py from transformers import ReactCodeAgent agent = ReactCodeAgent(tools=[], llm_engine=llm_engine, add_base_tools=True) agent.run("Why does Mike not know many people in New York?", audio="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/transformers/recording.mp3") ``` **检查Agent运行状况**。当Agent成功运行后,可以检查运行后发生的情况:agent.logs存储了智能体的详细日志。在Agent运行的每一步,所有内容都会被存储在一个字典中,然后追加到agent.logs中。

运行agent.write_inner_memory_from_logs()会为代理的日志创建一个内部存储,并化作聊天消息列表供LLM查看。此方法会遍历日志的每一步,并仅将感兴趣的内容存储为消息,它会将系统提示和任务保存在不同的消息中;对于每一步,它还会将LLM输出存储为一条消息,工具调用输出存储为另一条消息。如果想在更高层次的视角来了解发生了什么,可以使用这个方法,但并非所有日志都会被此方法转录。

13.4.5 练习12:Smolagents与Qwen实现文生图

源代码解读。使用独立的Smolagents创建一个简单的Gradio应用代理,该代理可以使用文本生成图像的工具。读者可对比Smolagents和Transformer.agent的区别,两者接口极其相似,只是实现细节有所差别,如代码13-28所示:

代码13-28
import gradio as gr
from dataclasses import asdict
from smolagents import Tool, CodeAgent, InferenceClientModel
from smolagents import stream_to_gradio
from huggingface_hub import login
import os

os.environ["HF_TOKEN"] = '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'
login(os.environ.get("HF_TOKEN"))
image_generation_tool = Tool.from_space(
    space_id="black-forest-labs/FLUX.1-dev",
    name="image_generator",
    description="Generates an image following your prompt. Returns a PIL Image.",
    api_name="/infer"
)
llm_engine = InferenceClientModel("Qwen/Qwen2.5-Coder-32B-Instruct")
agent = CodeAgent(tools=[image_generation_tool], model=llm_engine)

def interact_with_agent(prompt, history):
    messages = []
    yield messages
    for msg in stream_to_gradio(agent, prompt):
        if hasattr(msg, '__dict__'):
            messages.append(asdict(msg))
        else:
            messages.append(msg)
        yield messages
    yield messages

demo = gr.ChatInterface(interact_with_agent,
    chatbot= gr.Chatbot(label="Agent",
        avatar_images=(
            None,
            "https://em-content.zobj.net/source/twitter/53/robot-face_1f916.png")),
    examples=[["Generate an image of an astronaut riding an alligator"],
        ["I am writing a children's book for my daughter. Can you help me with some illustrations?"]])
if __name__ == "__main__":
    demo.launch(debug=True)

代码构建一个具备图像生成和代码处理能力的AI代理,讲解如下:
(1)导入transformers和gradio中的必要工具和类,其中函数stream_to_gradio使用代理运行提示任务,并将代理消息作为gr.ChatMessages实现流式传输。
(2)从Hub中导入登录函数login,登录后可获得更长token。通过Tool.from_space加载了FLUX.1-dev平台的图像生成API,API端点为/infer。使用Qwen2.5-Coder-32B-Instruct模型定义大模型引擎,该模型特别针对代码相关任务进行了优化。随后使用绘画工具和大模型引擎创建Agent。
(3)定义与代理交互的函数interact_with_agent,输入prompt和history之后,利用函数stream_to_gradio从代理中产生消息,消息转换为字典格式并添加到已输出消息,实时更新消息历史并返回。以上是和transformers代理相关的部分。
(4)最后通过gr.ChatInterface和gr.Chatbot定义Gradio UI,并与交互函数interact_with_agent绑定,同时添加type和examples等内容。
运行。要求transformers==4.47.0,gradio==5.49.1,运行界面如图13-5所示:
在这里插入图片描述

图13-5

从输出可以看到显示思考、工具调用和输出结果的过程,正是ReactCodeAgent的推理过程。也可在Hugging Face查看:gradio/agent_chatbot🖇️链接13-19

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐