6.5.4 返回结构化输出
在实际应用中,大多数代理默认返回单个字符串。然而,在某些情况下,返回具有更多结构化信息的输出可能会非常有用。在LangChain中,返回结构化输出是一种让代理(agent)提供更有组织和易于处理信息的方法。不同于简单的文本字符串,结构化输出可以包含多个字段,每个字段都承载着特定的数据。这种方式对于需要将代理的输出进一步用于数据分析、存储到数据库或集成到其他应用程序中的场景非常有用。
在LangChain中返回结构化输出的基本步骤如下所示:
(1)定义响应模式(Response Schema):首先,需要定义一个响应模式,这通常是一个类,使用Pydantic库来确保数据的类型和结构。例如,对于一个问答代理,可能需要一个包含答案和来源列表的响应模式。
from pydantic import BaseModel, Field
from typing import List
class Response(BaseModel):
answer: str = Field(description="The final answer to respond to the user")
sources: List[int] = Field(description="List of page chunks that contain the answer")
(2)创建代理(Creating the Agent):在创建代理时需要将工具(tools)和响应模式作为函数传递给语言模型(LLM),这样代理就可以在执行过程中调用这些工具,并按照定义好的模式返回结构化输出。
(3)自定义解析逻辑(Custom Parsing Logic):为了处理语言模型的输出并将其转换为结构化格式,可能需要自定义解析逻辑,这通常涉及到检查输出中的特定标记或函数调用,并据此构造相应的响应模式实例。
(4)运行代理(Running the Agent):在创建代理后可以通过提供输入来运行它,代理将执行其任务,并使用自定义解析逻辑来生成结构化输出。
(5)处理输出(Handling the Output):代理返回的结构化输出可以被应用程序的其他部分直接使用,无需额外的解析或转换步骤。
例如下面的实例演示了使用LangChain代理返回结构化输出的过程,这个例子的核心是构建一个能够理解和响应特定查询的智能系统,通过加载文档、分割文本、创建检索器和定义响应模型来实现。例子的最后部分展示了使用这个代理来执行一个具体查询的用法。
实例6-1:使用LangChain代理返回结构化输出(源码路径:codes\6\Returning.py)
实例文件Returning.py的具体实现代码如下所示。
from langchain_chroma import Chroma
from langchain_community.document_loaders import TextLoader
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
import json
from langchain_core.agents import AgentActionMessageLog, AgentFinish
from langchain.agents import AgentExecutor
from langchain.agents.format_scratchpad import format_to_openai_function_messages
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_openai import ChatOpenAI
from typing import List
from langchain_core.pydantic_v1 import BaseModel, Field
loader = TextLoader("state_of_the_union.txt")
documents = loader.load()
# 将文档分割成块
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
texts = text_splitter.split_documents(documents)
# 在这里我们添加虚假的源信息
for i, doc in enumerate(texts):
doc.metadata["page_chunk"] = i
# 创建我们的检索器
embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_documents(texts, embeddings, collection_name="state-of-union")
retriever = vectorstore.as_retriever()
from langchain.tools.retriever import create_retriever_tool
retriever_tool = create_retriever_tool(
retriever,
"state-of-union-retriever",
"查询检索器以获取国情咨文的信息",
)
class Response(BaseModel):
answer: str = Field(description="最终回答用户的答案")
sources: List[int] = Field(
description="包含问题答案的页面块列表。只有在页面块包含相关信息时才包括它"
)
def parse(output):
# 如果没有调用函数,返回给用户
if "function_call" not in output.additional_kwargs:
return AgentFinish(return_values={"output": output.content}, log=output.content)
# 解析函数调用
function_call = output.additional_kwargs["function_call"]
name = function_call["name"]
inputs = json.loads(function_call["arguments"])
# 如果调用了Response函数,使用函数输入返回给用户
if name == "Response":
return AgentFinish(return_values=inputs, log=str(function_call))
# 否则,返回代理动作
else:
return AgentActionMessageLog(
tool=name, tool_input=inputs, log="", message_log=[output]
)
prompt = ChatPromptTemplate.from_messages(
[
("system", "你是一个有用的助手"),
("user", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
]
)
llm = ChatOpenAI(temperature=0)
llm_with_tools = llm.bind_functions([retriever_tool, Response])
agent = (
{
"input": lambda x: x["input"],
# 从中间步骤格式化代理草稿板
"agent_scratchpad": lambda x: format_to_openai_function_messages(
x["intermediate_steps"]
),
}
| prompt
| llm_with_tools
| parse
)
agent_executor = AgentExecutor(tools=[retriever_tool], agent=agent, verbose=True)
agent_executor.invoke(
{"input": "总统对Ketanji Brown Jackson说了什么"},
return_only_outputs=True,
)
在上述代码中,
上述代码的实现流程如下所示:
(1)文档加载与处理:使用TextLoader加载一个文本文件(state_of_the_union.txt),这通常包含国情咨文的文本。通过RecursiveCharacterTextSplitter将文本分割成较小的块,以便更好地管理和处理。
(2)添加元数据:为每个文本块添加元数据,这里特别添加了page_chunk,用于标识每个块的编号。
(3)创建向量存储和检索器
- 使用OpenAIEmbeddings生成文本的嵌入表示
- 利用嵌入表示和文档创建Chroma向量存储,这允许高效的相似性搜索。
- 从向量存储创建一个检索器(retriever),用于后续查询。
(4)创建检索器工具:使用create_retriever_tool函数创建一个工具,该工具允许用户通过查询检索器来获取信息。
(5)定义响应模型:定义类Response,这是一个继承自BaseModel的Pydantic模型,用于结构化输出。它包含一个答案字符串和一个源页面块列表。
(6)解析输出:实现方法parse,用于解析代理的输出。如果输出中包含了方法调用,它会根据方法的名称和参数进行相应的处理。
(7)构建提示模板:使用ChatPromptTemplate构建一个用于与代理交互的提示模板,包括系统描述、用户输入和代理草稿板。
(8)绑定大型语言模型(LLM):使用ChatOpenAI创建一个大型语言模型实例,并将其与检索器工具和Response函数绑定。
(9)构建代理:构建一个代理,它接收输入,能够格式化草稿板,使用LLM处理输入,并最终通过parse函数解析输出结果。
(10)执行代理:使用AgentExecutor执行代理,并提供必要的工具和设置。
(11)调用代理:最后,通过invoke方法调用代理,传入用户提出的问题,并请求仅返回输出。
执行后会输出:
AgentFinish(return_values={'output': 'No information found about Ketanji Brown Jackson in the provided text.'}, log='No information found about Ketanji Brown Jackson in the provided text.')