结构化结果和工具调用#
结构化结果#
LLM返回结构化数据这一功能极具实用价值,例如从数据中提取接口参数,亦或是将数据存储于数据库。 接下来介绍几种相关方法
with_structured_output方法#
这是模型所提供的原生 API 用于输出结构化数据,此方式是最为简单且可靠的。不管是工具调用、函数调用还是 json 模型,其底层实现均采用这种模式。 有一部分模型并不支持这一方法,而支持的 LLM 如下所示: https://python.langchain.com/v0.2/docs/integrations/chat/
from langchain_openai import ChatOpenAI
from langchain_core.pydantic_v1 import BaseModel, Field
from typing import Optional
# 开启debug
from langchain.globals import set_debug
set_debug(True)
llm = ChatOpenAI(model="gpt-3.5-turbo-0125")
class Joke(BaseModel):
"""Joke to tell user."""
setup: str = Field(description="The setup of the joke")
punchline: str = Field(description="The punchline to the joke")
rating: Optional[int] = Field(description="How funny the joke is, from 1 to 10")
structured_llm = llm.with_structured_output(Joke)
structured_llm.invoke("Tell me a joke about cats")
可以看到上面在模型的返回中text
是没有值的,在kwargs.additional_kwargs.tool_calls
中返回了json,按照既定格式要求返回了数据。LangSmith如下:
可以看到,这里将模型转为了
函数调用
,所以,这里的实现还是 function tools,查看with_structured_output
源码可以证明
也可以将schema的定义通过json的形式传递来,这样返回值是dict
json_schema = {
"title": "joke",
"description": "Joke to tell user.",
"type": "object",
"properties": {
"setup": {
"type": "string",
"description": "The setup of the joke",
},
"punchline": {
"type": "string",
"description": "The punchline to the joke",
},
"rating": {
"type": "integer",
"description": "How funny the joke is, from 1 to 10",
},
},
"required": ["setup", "punchline"],
}
structured_llm = llm.with_structured_output(json_schema)
structured_llm.invoke("Tell me a joke about cats")
在多个返回值类型中选择#
可以规定多个类型,让模型在里面做选择,如下所示
from typing import Union
class ConversationalResponse(BaseModel):
"""Respond in a conversational manner. Be kind and helpful."""
response: str = Field(description="A conversational response to the user's query")
class Response(BaseModel):
output: Union[Joke, ConversationalResponse]
structured_llm = llm.with_structured_output(Response)
structured_llm.invoke("Tell me a joke about cats")
# 选择了 Joke
# 选择了ConversationalResponse
structured_llm.invoke("How are you today?")
Streaming#
也支持流式输出
structured_llm = llm.with_structured_output(Response)
set_debug(False)
for chunk in structured_llm.stream("Tell me a joke about cats"):
print(chunk,type(chunk))
Few-shot prompting#
对于更复杂的模式,向提示添加少量示例非常有用。这可以通过几种方式来实现。 最简单的方式是将Example直接添加到system中
from langchain_core.prompts import ChatPromptTemplate
set_debug(True)
system = """You are a hilarious comedian. Your specialty is knock-knock jokes. \
Return a joke which has the setup (the response to "Who's there?") and the final punchline (the response to "<setup> who?").
Here are some examples of jokes:
example_user: Tell me a joke about planes
example_assistant: {{"setup": "Why don't planes ever get tired?", "punchline": "Because they have rest wings!", "rating": 2}}
example_user: Tell me another joke about planes
example_assistant: {{"setup": "Cargo", "punchline": "Cargo 'vroom vroom', but planes go 'zoom zoom'!", "rating": 10}}
example_user: Now about caterpillars
example_assistant: {{"setup": "Caterpillar", "punchline": "Caterpillar really slow, but watch me turn into a butterfly and steal the show!", "rating": 5}}"""
prompt = ChatPromptTemplate.from_messages([("system", system), ("human", "{input}")])
few_shot_structured_llm = prompt | structured_llm
few_shot_structured_llm.invoke("what's something funny about woodpeckers")
(Advanced) Specifying the method for structuring outputs#
是用json_mode
的形式,需要在promot中指定JSON返回值,并且schema的key。
with_structured_output
支持两种模型,一种是function_calling
如上所述,一种是json_mode
, 在json_mode
下,不会将schema包装为function tools给llm,而是一个promot,所以需要在promot中规定返回的json格式。源码和LangSmith如下
structured_llm = llm.with_structured_output(Joke, method="json_mode")
structured_llm.invoke(
"Tell me a joke about cats, respond in JSON with `setup` and `punchline` keys"
)
(Advanced) Raw outputs#
LLMs 在生成结构化输出时并非尽善尽美,尤其是当模式趋于复杂的时候。您可以通过传递 include_raw=True 这一参数,以避免引发异常,并自行去处理原始输出。这样做将会改变输出的格式,其中会包含原始消息输出(raw)、解析后的值(若成功解析)(parsed)以及任何产生的错误(parsing_error)。
structured_llm = llm.with_structured_output(Joke, include_raw=True)
res = structured_llm.invoke(
"Tell me a joke about cats, respond in JSON with `setup` and `punchline` keys"
)
print(res)
res.keys()
res["parsed"]
自己写promot,自己解析输出值#
并非所有模型都支持 .with_structured_output() 这一功能,原因在于并非所有模型都支持工具调用或 JSON 模式。对于这类模型,你的直接提示模型采用特定的格式,同时使用输出解析器从原始的模型输出里提取结构化的响应。
PydanticOutputParser#
from typing import List
from langchain_core.output_parsers import PydanticOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.pydantic_v1 import BaseModel, Field
class Person(BaseModel):
"""Information about a person."""
name: str = Field(..., description="The name of the person")
height_in_meters: float = Field(
..., description="The height of the person expressed in meters."
)
class People(BaseModel):
"""Identifying information about all people in a text."""
people: List[Person]
# Set up a parser
parser = PydanticOutputParser(pydantic_object=People)
prompt_template = ChatPromptTemplate.from_messages(
[
(
"system",
"Answer the user query. Wrap the output in `json` tags\n{format_instructions}",
),
("human", "{query}"),
]
).partial(format_instructions=parser.get_format_instructions())
query = "Anna is 23 years old and she is 6 feet tall"
print(prompt_template.invoke(query).to_string())
# 代入llm
chain = prompt_template | llm | parser
chain.invoke({"query": query})
Custom Parsing#
还可以使用 LangChain 表达式语言(LCEL)创建自定义提示和解析器,通过一个普通函数来解析来自模型的输出。 这种方式是llm刚开始的阶段,不推荐使用。
import json
import re
from typing import List
from langchain_core.messages import AIMessage
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.pydantic_v1 import BaseModel, Field
class Person(BaseModel):
"""Information about a person."""
name: str = Field(..., description="The name of the person")
height_in_meters: float = Field(
..., description="The height of the person expressed in meters."
)
class People(BaseModel):
"""Identifying information about all people in a text."""
people: List[Person]
# Prompt
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"Answer the user query. Output your answer as JSON that "
"matches the given schema: ```json\n{schema}\n```. "
"Make sure to wrap the answer in ```json and ``` tags",
),
("human", "{query}"),
]
).partial(schema=People.schema())
# Custom parser
def extract_json(message: AIMessage) -> List[dict]:
"""Extracts JSON content from a string where JSON is embedded between ```json and ``` tags.
Parameters:
text (str): The text containing the JSON content.
Returns:
list: A list of extracted JSON strings.
"""
text = message.content
# Define the regular expression pattern to match JSON blocks
pattern = r"```json(.*?)```"
# Find all non-overlapping matches of the pattern in the string
matches = re.findall(pattern, text, re.DOTALL)
# Return the list of matched JSON strings, stripping any leading or trailing whitespace
try:
return [json.loads(match.strip()) for match in matches]
except Exception:
raise ValueError(f"Failed to parse: {message}")
query = "Anna is 23 years old and she is 6 feet tall"
print(prompt.format_prompt(query=query).to_string())
chain = prompt | llm | extract_json
chain.invoke({"query": query})
如何使用chat models来调用工具#
官方文档: https://python.langchain.com/v0.2/docs/how_to/custom_tools/#creating-tools-from-functions
将tools传递给llm#
@tool
用来定义工具, 在定义的时候需要通过类型参数,文档注释等等来声明工具的作用,名字,参数,返回只等,在默认状况下,此装饰器会把函数名当作工具名。
from langchain_core.tools import tool
@tool(return_direct=True)
def add(a: int, b: int) -> int:
"""Adds a and b."""
return a + b
@tool
def multiply(a: int, b: int) -> int:
"""Multiplies a and b."""
return a * b
print(type(add))
print(add.name)
print(add.args)
print(add.description)
# 声明工具,传递给llm
tools = [add, multiply]
llm_with_tools = llm.bind_tools(tools)
query = "What is 3 * 12? Also, what is 11 + 49?"
ai_msg = llm_with_tools.invoke(query)
要注意:LLM本身没有调用函数的能力,他的返回值只是告诉我们需要调用函数,调用哪些函数,入参是什么。
这些信息都封装在tool_calls
方法中
ai_msg.tool_calls
说回到tool,他的类型是StructuredTool
,他也是实现了Runnable
接口,所以,他是可以直接通过invoke
调用的, 可以直接在LCEL中使用的
add.invoke({"a":1,"b":1})
okk,对于上面的结果,就可以找到对应的tool,将llm中的调用tool的参数塞进去调用就完事。
for tool_call in ai_msg.tool_calls:
selected_tool = {"add": add, "multiply": multiply}[tool_call["name"].lower()]
tool_msg = selected_tool.invoke(tool_call["args"])
print(tool_msg)
end