LangChain工具链及稳定结构化输出

7/8/2023 LangChainLangFlowXGrammarTypeChat结构化输出LLM工具链

# 1. LangChain框架的介绍与使用

# 1.1 LangChain简介

项目简介:LangChain是一个开源的框架,它可以让开发人员把像GPT-4这样的大型语言模型(LLM)和外部数据结合起来,它提供了Python或JavaScript的包。ChatGPT模型是用到2021年的数据训练的,这可能会有很大的局限性。虽然这些模型的通用知识很棒,但是如果能让它们连接到自定义的数据和计算,就会有更多的可能性,这就是LangChain做的事情。简单来说,它可以让你的LLM在回答问题时参考整个数据库。所以你现在可以让你的GPT模型访问最新的数据,比如报告、文档和网站信息。

LangChain的功能模块

# 1.2 LangChain基本概念

# 1.2.1 Loader 加载器

顾名思义,这个就是从指定源进行加载数据的。比如:文件夹 DirectoryLoader、Azure 存储 AzureBlobStorageContainerLoader、CSV文件 CSVLoader、印象笔记 EverNoteLoader、Google网盘 GoogleDriveLoader、任意的网页 UnstructuredHTMLLoader、PDF PyPDFLoader、S3 S3DirectoryLoaderS3FileLoader、Youtube YoutubeLoader 等。

# 1.2.2 Document 文档

当使用 Loader 加载器读取到数据源后,数据源需要转换成 Document 文档对象后,后续才能进行使用。

# 1.2.3 Text Spltters 文本分割

顾名思义,Text Spltters 就是用来分割文本的,因为 OpenAI API 是有字符限制的。比如我们将一份300页的 pdf 发给 OpenAI API,让它进行总结,肯定会报超过最大 Token 的错误,所以这里就需要使用文本分割器去分割我们 Loader 进来的 Document 文档对象。

# 1.2.4 Vector Stores 向量数据库

因为数据相关性搜索其实是向量运算。所以需要将我们的加载进来的数据 Document 文档对象进行向量化,才能进行向量运算搜索。转换成向量也很简单,只需要我们把数据存储到对应的向量数据库中即可完成向量的转换。

# 1.2.5 Chain 链

我们可以把 Chain 理解为任务。一个 Chain 就是一个任务,当然也可以像链条一样,一个一个的执行多个链。

# 1.2.6 Agent 代理

可以简单的理解为它可以动态的帮我们选择和调用Chain或者已有的工具,执行过程可以参考下面这张图:

LangChain的Agent代理

# 1.2.7 Embedding 嵌入

用于衡量文本的相关性,这个也是 OpenAI API 能实现构建自己知识库的关键所在。它相比 fine-tuning 最大的优势就是,不用进行训练,并且可以实时添加新的内容,而不用加一次新的内容就训练一次,并且各方面成本要比 fine-tuning 低很多。

# 1.3 LangChain文档解析

实验环境:Macbook Pro 2021,M1 pro芯片,16G内存,macOS Ventura13.2.1系统,Python3.9环境

以下均使用LangChain对文档进行解析,部分类型需要额外补充安装OCR依赖。

# 1.3.1 TXT内容解析

# -*- coding: utf-8 -*-

from langchain.document_loaders import UnstructuredFileLoader

loader = UnstructuredFileLoader("./data/test.txt")
docs = loader.load()
for doc in docs:
    print(doc.page_content)
1
2
3
4
5
6
7
8

# 1.3.2 Markdown内容解析

Markdown的解析和TextLoader是存在区别的,主要是需要设置mode和autodetect_encoding参数,以区分文件中不同块的信息。

# -*- coding: utf-8 -*-

from langchain.document_loaders import UnstructuredFileLoader

loader = UnstructuredFileLoader("./data/test.md", mode="elements", autodetect_encoding=True)
docs = loader.load()
for doc in docs:
    print(doc.page_content)
1
2
3
4
5
6
7
8

# 1.3.3 Word内容解析

这里支持docx格式,而不支持doc格式。

# -*- coding: utf-8 -*-

from langchain.document_loaders import UnstructuredWordDocumentLoader
loader = UnstructuredWordDocumentLoader("./data/test.docx")
docs = loader.load()
for doc in docs:
    print(doc.page_content)
1
2
3
4
5
6
7

# 1.3.4 PDF内容解析

[1] 解析离线PDF

基于Unstructured库

# -*- coding: utf-8 -*-

from langchain_community.document_loaders import UnstructuredFileLoader

loader = UnstructuredFileLoader("./data/test.pdf", mode="elements")
docs = loader.load()
for doc in docs:
    print(doc.page_content)
1
2
3
4
5
6
7
8

基于PyPDF库

# -*- coding: utf-8 -*-

from langchain.document_loaders import PyPDFLoader

loader = PyPDFLoader("./data/test.pdf")
docs = loader.load()
for doc in docs:
    print(doc.page_content)
1
2
3
4
5
6
7
8

基于PDFMiner库

# -*- coding: utf-8 -*-

from langchain.document_loaders import PDFMinerLoader
loader = PDFMinerLoader("./data/test.pdf")
docs = loader.load()
for doc in docs:
    print(doc.page_content)
1
2
3
4
5
6
7

[2] 解析在线PDF

# -*- coding: utf-8 -*-

from langchain.document_loaders import OnlinePDFLoader
loader = OnlinePDFLoader("https://arxiv.org/pdf/2302.03803.pdf")
docs = loader.load()
for doc in docs:
    print(doc.page_content)
1
2
3
4
5
6
7

# 1.3.5 PPT内容解析

# -*- coding: utf-8 -*-

from langchain.document_loaders import UnstructuredPowerPointLoader
loader = UnstructuredPowerPointLoader("./data/test.pptx")
docs = loader.load()
for doc in docs:
    print(doc.page_content)
1
2
3
4
5
6
7

# 1.3.6 图片内容解析

# -*- coding: utf-8 -*-

import pytesseract
from PIL import Image
from langchain_community.document_loaders import UnstructuredImageLoader

# 设置 Tesseract 配置参数,使用中文语言包
custom_config = r'--oem 3 --psm 6 -l chi_sim'

# 自定义图像加载器,使用 Tesseract OCR 进行文本提取
class CustomUnstructuredImageLoader(UnstructuredImageLoader):
    def _get_elements(self):
        elements = []
        image = Image.open(self.file_path)
        text = pytesseract.image_to_string(image, config=custom_config)
        elements.append(text)
        return elements

loader = CustomUnstructuredImageLoader("./data/test.png")
datas = loader.load()
for data in datas:
    print(data.page_content)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 1.4 LangChain使用实例

实验环境:Macbook Pro 2021,M1 pro芯片,16G内存,macOS Ventura13.2.1系统,Python3.9环境

以下使用的 LLM 都是以 Open AI 的 ChatGPT 为例,以 ipynb 的方式运行。账号与API-KEY自己注册或直接购买,如果 LLM 不想使用 ChatGPT,可以根据自己任务的需要换成自己需要的 LLM 模型即可。

# 1.4.1 完成一次问答

第一个案例就来个最简单的,用 LangChain 加载 OpenAI 的模型,并且完成一次问答。在开始之前,我们需要先设置我们的 OpenAI 的 key。

import os
os.environ["OPENAI_API_KEY"] = 'YOUR_OPENAI_API_KEY'
1
2

然后,我们进行导入和执行:

from langchain.llms import OpenAI

llm = OpenAI(model_name="text-davinci-003",max_tokens=1024)
llm("怎么评价人工智能")
1
2
3
4

LangChain完成一次问答

# 1.4.2 使用 Memory 使会话带有记忆

可以使用自带的 Memory 来实现会话记忆功能。

import os
os.environ["OPENAI_API_KEY"] = 'YOUR_OPENAI_API_KEY'

from langchain.memory import ChatMessageHistory
from langchain.chat_models import ChatOpenAI

chat = ChatOpenAI(temperature=0, openai_api_base="YOUR_OPENAI_API_KEY_BASE")

# 初始化 MessageHistory 对象
history = ChatMessageHistory()

# 给 MessageHistory 对象添加对话内容
history.add_ai_message("解释一下量子计算")
history.add_user_message("它与量子力学什么关系?")

# 执行对话
ai_response = chat(history.messages)
print(ai_response)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

LangChain使用Memory使会话带有记忆

# 1.4.3 通过 Google 搜索并返回答案

让我们的 OpenAI API 联网搜索,并返回答案给我们。这里我们需要借助 Serpapi 来进行实现,Serpapi 提供了 Google 搜索的 API。首先需要到 Serpapi官网 (opens new window)上注册一个用户,并复制他给我们生成 API-KEY,然后将其配置到环境变量里面去。

import os
os.environ["OPENAI_API_KEY"] = 'YOUR_OPENAI_API_KEY'
os.environ["SERPAPI_API_KEY"] = 'YOUR_SERPAPI_API_KEY'
1
2
3

然后,我们进行导入和执行:

from langchain.agents import load_tools
from langchain.agents import initialize_agent
from langchain.llms import OpenAI
from langchain.agents import AgentType

# 加载 OpenAI 模型
llm = OpenAI(temperature=0,max_tokens=2048) 

 # 加载 serpapi 工具
tools = load_tools(["serpapi"])

# 工具加载后都需要初始化,verbose 参数为 True,会打印全部的执行详情
agent = initialize_agent(tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True)

# 运行 agent
agent.run("What's the date today? What great events have taken place today in history?")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

注:这个 Serpapi 对中文不是很友好,所以提问的 prompt 建议使用英文。

LangChain使用Google搜索

它将我们的问题拆分成了几个步骤,然后一步一步得到最终的答案,正确的返回了日期(有时差),并且返回了历史上的今天。

在 chain 和 agent 对象上都会有 verbose 这个参数,这个是个非常有用的参数,开启它之后可以看到完整的 chain 执行过程。

关于 agent type 几个选项的含义:

  • zero-shot-react-description:根据工具的描述和请求内容的来决定使用哪个工具(最常用)。
  • react-docstore:使用 ReAct 框架和 docstore 交互, 使用SearchLookup 工具,前者用来搜,后者寻找term, 举例: Wipipedia 工具。
  • self-ask-with-search 此代理只使用一个工具:Intermediate Answer,它会为问题寻找事实答案(指的非 GPT 生成的答案,而是在网络文本中已存在的), 如 Google search API 工具。
  • conversational-react-description:为会话设置而设计的代理,它的prompt会被设计的具有会话性,且还是会使用 ReAct 框架来决定使用来个工具,并且将过往的会话交互存入内存。

# 1.4.4 对超长文本进行总结

假如想要用 OpenAI API 对一个段文本进行总结,我们通常的做法就是直接发给 API 让他总结,但是如果文本超过了 API 最大的 token 限制就会报错。这时,我们一般会进行对文章进行分段,比如通过 tiktoken 计算并分割,然后将各段发送给 API 进行总结,最后将各段的总结再进行一个全部的总结。LangChain很好的帮我们处理了这个过程,使得我们编写代码变的非常简单。

from langchain.document_loaders import UnstructuredFileLoader
from langchain.chains.summarize import load_summarize_chain
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain import OpenAI

# 导入文本
loader = UnstructuredFileLoader("/your_path/long_text.txt")
# 将文本转成 Document 对象
document = loader.load()
print(f'documents:{len(document)}')

# 初始化文本分割器
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size = 500,
    chunk_overlap = 0
)

# 切分文本
split_documents = text_splitter.split_documents(document)
print(f'documents:{len(split_documents)}')

# 加载 llm 模型
llm = OpenAI(max_tokens=1500)

# 创建总结链
chain = load_summarize_chain(llm, chain_type="refine", verbose=True)

# 执行总结链,(为了快速演示,只总结前5段)
chain.run(split_documents[:5])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

首先我们对切割前和切割后的 document 个数进行了打印,可以看到,切割前就是只有整篇的一个 document,切割完成后,会把上面一个 document 切成 59 个 document。为了快速演示,这里最终只输出了前 5 个 document 的总结。

LangChain对超长文本进行总结

这里有几个参数需要注意:

[1] 文本分割器的 chunk_overlap参数

这个是指切割后的每个 document 里包含几个上一个 document 结尾的内容,主要作用是为了增加每个 document 的上下文关联。比如,chunk_overlap=0时, 第一个 document 为 aaaaaa,第二个为 bbbbbb;当 chunk_overlap=2 时,第一个 document 为 aaaaaa,第二个为 aabbbbbb。不过,这个也不是绝对的,要看所使用的那个文本分割模型内部的具体算法。

[2] chain 的 chain_type 参数

这个参数主要控制了将 document 传递给 llm 模型的方式,一共有 4 种方式:

  • stuff: 这种最简单粗暴,会把所有的 document 一次全部传给 llm 模型进行总结。如果document很多的话,势必会报超出最大 token 限制的错,所以总结文本的时候一般不会选中这个。

  • map_reduce: 这个方式会先将每个 document 进行总结,最后将所有 document 总结出的结果再进行一次总结。

    chain的chain_type参数map_reduce取值

  • refine: 这种方式会先总结第一个 document,然后在将第一个 document 总结出的内容和第二个 document 一起发给 llm 模型在进行总结,以此类推。这种方式的好处就是在总结后一个 document 的时候,会带着前一个的 document 进行总结,给需要总结的 document 添加了上下文,增加了总结内容的连贯性。

    chain的chain_type参数refine取值

  • map_rerank: 这种一般不会用在总结的 chain 上,而是会用在问答的 chain 上,他其实是一种搜索答案的匹配方式。首先你要给出一个问题,他会根据问题给每个 document 计算一个这个 document 能回答这个问题的概率分数,然后找到分数最高的那个 document ,在通过把这个 document 转化为问题的 prompt 的一部分(问题+document)发送给 llm 模型,最后 llm 模型返回具体答案。

# 1.4.5 构建本地知识库问答机器人

在这个例子中,介绍如何从我们本地读取多个文档构建知识库,并且使用 OpenAI API 在知识库中进行搜索并给出答案。这个非常有用的,比如可以很方便的做一个可以介绍公司产品的机器人。

from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.text_splitter import CharacterTextSplitter
from langchain import OpenAI,VectorDBQA
from langchain.document_loaders import DirectoryLoader

# 加载文件夹中的所有txt类型的文件
loader = DirectoryLoader('/your_path/doc', glob='**/*.pdf')
# 将数据转成 document 对象,每个文件会作为一个 document
documents = loader.load()

# 初始化加载器
text_splitter = CharacterTextSplitter(chunk_size=100, chunk_overlap=0)
# 切割加载的 document
split_docs = text_splitter.split_documents(documents)

# 初始化 openai 的 embeddings 对象
embeddings = OpenAIEmbeddings()
# 将 document 通过 openai 的 embeddings 对象计算 embedding向量信息并临时存入 Chroma 向量数据库,用于后续匹配查询
docsearch = Chroma.from_documents(split_docs, embeddings)

# 创建问答对象
qa = VectorDBQA.from_chain_type(llm=OpenAI(), chain_type="stuff", vectorstore=docsearch,return_source_documents=True)
# 进行问答
result = qa({"query": "Kafka为什么基于磁盘还那么快"})
print(result)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

可以通过结果看到,它成功的从我们的给到的文档中获取了正确的答案。

LangChain构建本地知识库问答机器人

# 1.4.6 爬取网页并输出JSON数据

有些时候我们需要爬取一些结构性比较强的网页,并且需要将网页中的信息以JSON的方式返回回来,可以使用 LLMRequestsChain 类去实现。

from langchain.prompts import PromptTemplate
from langchain.llms import OpenAI
from langchain.chains import LLMRequestsChain, LLMChain

llm = OpenAI(model_name="gpt-3.5-turbo", temperature=0)

template = """在 >>> 和 <<< 之间是网页的返回的HTML内容。
网页是新浪财经A股上市公司的公司简介。
请抽取参数请求的信息。

>>> {requests_result} <<<
请使用如下的JSON格式返回数据
{{
  "company_name":"a",
  "company_english_name":"b",
  "issue_price":"c",
  "date_of_establishment":"d",
  "registered_capital":"e",
  "office_address":"f",
  "Company_profile":"g"
}}
Extracted:"""

prompt = PromptTemplate(
    input_variables=["requests_result"],
    template=template
)

chain = LLMRequestsChain(llm_chain=LLMChain(llm=llm, prompt=prompt))
inputs = {
  "url": "https://vip.stock.finance.sina.com.cn/corp/go.php/vCI_CorpInfo/stockid/600519.phtml"
}

response = chain(inputs)
print(response['output'])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

可以看到,它很好的将格式化后的爬取结果输出了出来:

LangChain爬取网页并输出JSON数据

# 2. LangFlow可视化工具链调用

# 2.1 LangFlow简介

Langflow 是一个LangChain UI,提供了一种交互界面来使用LangChain,通过拖拽即可搭建自己的实验、原型流。

这里使用的是当前最新的 Langflow 0.2.13 版本,由于 Langflow 目前更新迭代比较快,不保证能够向前、向后兼容。

LangFlow组件介绍

在左侧边栏中,可以看到所有你可以使用的工具:

  • Agent——将大语言模型的决策与工具结合起来,使其能够实施和执行行动;
  • Chains——允许组合人工智能应用程序的不同组件,如prompts,大语言模型和存储器;
  • Loaders——允许与外部源集成以上传信息,如PDF、CSV等;
  • Embeddings——将文本嵌入到向量潜在空间的模型;
  • LLMs——用于理解和生成文本的模型,如OpenAI模型;
  • Memories——LangChain组件,能够维护会话的记忆;
  • Prompts——用于定义prompts框架的模板;
  • Retrievers——用于信息检索的组件,通过倒排索引从大规模文本数据集中检索相关信息,以支持多轮对话系统的问答;
  • Text Splitters——将文本分割成更小的块并避免tokens限制问题的实用程序;
  • Toolkits——为特定用例与外部系统集成,例如,CSV文件或Python Agent;
  • Tools——代理可以用来与外部工具交互的功能,如必应搜索或本地文件系统;
  • Utilities——对Google API或Bing API等系统有用的包装器;
  • Vector Stores——向量数据库,文本Embeddings可以保存,然后与用户的输入进行匹配;
  • Wrappers——请求库的轻量级包装器。

# 2.2 LangFlow搭建

实验环境:Macbook Pro 2021,M1 pro芯片,16G内存,macOS Ventura13.3.1系统,Python3.9环境

LangFlow的安装与启动非常简单,Langflow启动的详细参数见官方文档:

$ pip3 install langflow
$ langflow
1
2

启动成功后,使用Chrome浏览器打开 http://127.0.0.1:7860 (opens new window) 地址即可使用。

注:如果需要对组件进行二次开发的话,在./langflow/src目录下有前后端的代码。

  • 后端安装好依赖,右键./langflow/src/backend目录,依次点击Mark Directory as、Sources Root,启动main.py。(默认http://localhost:7860/
  • 前端使用npm install安装好依赖后使用npm start启动。(默认http://localhost:3000/

# 2.3 LangFlow使用实例

LangFlow官方还有个项目是专门放置使用示例的,里面有很多现成的流程可以供我们入门时参考。

langflow_examples

注:在 LangFlow 里使用 OpenAI 组件时,将 OpenAl API Base 参数项配置成国内代理地址即可,这样可以大大加快速度。

# 2.3.1 基本OpenAI查询

打开LangFlow界面,点击 + New Project 按钮新建一个项目,然后点击左上方的 Import 按钮,导入 Basic Chat.json示例,之后填写 OpenAl APl Key,点击一下右下方的闪电按钮,构建成功后组建会变成绿色,右下角出现聊天按钮。

LangFlow基本OpenAI查询

点击右下角的聊天按钮,输入问题,即可实现 OpenAI 问答了。

LangFlow基本OpenAI查询的效果

# 2.3.2 记录历史的OpenAI查询

导入 Basic Chat with Prompt and History.json示例,之后填写 OpenAl APl Key,输入你要询问的问题。

记录历史的OpenAI查询

使用 ConversationBufferMemory 组件可以记录查询历史。可以看到从历史查询记录中获取到了“它”指代的是量子计算。

记录历史的OpenAI查询效果

# 2.3.3 使用Agent自动分解任务

导入 SerpAPI Tool.json示例,之后填写 OpenAl APl Key 和 Serpapi API Key,输入你要询问的问题。

使用Agent自动分解任务

ZeroShotAgent组件可以自动将用户问题进行分解。可以看到,提出问题后它自动给分解成几个子问题,查询Google,然后总结结果。

使用Agent自动分解任务的效果

# 2.4 在Python里调用导出的流程

在界面上点击Export按钮,会弹出导出弹框,记得勾选上“Save with my APl keys”,然后点击Download Flow按钮,会下载一个JSON格式的流程文件。

LangFlow导出JSON流程

导出的流程在Python代码里调用非常简单,只需要以下三行代码,可以进一步将其封装成服务。

from langflow import load_flow_from_json

flow = load_flow_from_json("/your_path/your_flow.json")
flow("解释一下量子计算")
1
2
3
4

调用导出的LangFlow流程

# 3. LLM稳定结构化输出

# 3.1 OpenAI支持稳定结构化输出

# 3.1.1 gpt-4o-2024-08-06模型

通过设置 "strict": true 参数,可以实现结构化输出,模型输出将与提供的格式定义相匹配,此功能适用于OpenAI的所有模型。其中,OpenAI刚发布的gpt-4o-2024-08-06模型,可以实现输出JSON的100%准确率。

gpt-4o-2024-08-06结构化输出

# 3.1.2 结构化输出实现原理

OpenAI的大模型结构化输出主要依赖于两种关键技术来实现高精度和可靠性的JSON输出:专项培训和约束解码。

[1] 专项培训

  • 这是比较传统的方法,OpenAI对特定的复杂JSON架构进行了针对性模型训练。这种方法的核心是通过专项数据集的反复训练,提升模型对特定格式的理解和输出能力。结果是模型的JSON输出准确率从最初的40%提升到了93%。尽管这一准确率已经有了显著提高,但对于开发人员来说,7%的错误率仍然是不可接受的,因此,这种方法仅仅作为基础手段存在。

[2] 约束解码

  • 为了进一步提高模型输出的可靠性,OpenAI引入了约束解码技术。通常,大模型在生成文本时,会在词汇表中选择下一个token,但这种选择是基于概率的,存在不可控性,这就可能导致格式错误。约束解码技术通过在模型生成下一个token时引入额外的约束条件,将模型的选择范围限制在有效的token集合内,确保生成的内容符合预定的格式和结构。
  • 例如,当输入“{"val”时,模型被限制为只能生成有效的JSON格式的下一个字符,而不会生成无效字符。这种动态约束使得大模型可以实现100%的JSON格式正确性,并精确遵循指定的Schema结构。

# 3.1.3 目前的限制与缺陷

尽管这种方法大幅提高了JSON输出的准确性,但也带来了一些限制和缺陷:

  • 性能限制:使用结构化输出时,需要额外的Schema预处理时间,这会导致生成速度变慢。
  • 支持的JSON模式有限:当前仅支持String、Number、Boolean、Object、Array、Enum和anyOf类型的JSON模式。
  • 对象嵌套深度和大小有限制:最多支持5级嵌套,且一个架构最多有100个对象属性。
  • 不能防止所有错误:尽管格式上可以做到100%正确,但模型可能在值的生成上(如数学公式)仍会犯错误,需要开发者在指令中提供更详细的示例或将任务拆分为更简单的子任务。
  • 安全性考虑:结构化输出功能依然遵循OpenAI的安全政策,对于不安全的请求模型会拒绝生成,并在API响应中设置一个新的字符串值,供开发者检测。

# 3.1.4 结构化输出测试示例

测试请求:

$ curl https://api.openai.com/v1/chat/completions \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer sk-xxx" \       // 更换API-KEY
    -d '{}'                                   // 更换body请求体
1
2
3
4

输入示例:

{
  "model": "gpt-4o-2024-08-06",
  "messages": [
    {
      "role": "system",
      "content": "You are a helpful math tutor."
    },
    {
      "role": "user",
      "content": "solve 8x + 31 = 2"
    }
  ],
  "response_format": {
    "type": "json_schema",
    "json_schema": {
      "name": "math_response",
      "strict": true,
      "schema": {
        "type": "object",
        "properties": {
          "steps": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "explanation": {
                  "type": "string"
                },
                "output": {
                  "type": "string"
                }
              },
              "required": ["explanation", "output"],
              "additionalProperties": false
            }
          },
          "final_answer": {
            "type": "string"
          }
        },
        "required": ["steps", "final_answer"],
        "additionalProperties": false
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

结构化输出:

{
  "steps": [
    {
      "explanation": "We start with the equation 8x + 31 = 2. Our goal is to solve for x by isolating it on one side of the equation.",
      "output": "8x + 31 = 2"
    },
    {
      "explanation": "Subtract 31 from both sides to get rid of the constant term on the left side.",
      "output": "8x = 2 - 31"
    },
    {
      "explanation": "Calculate 2 - 31 to simplify the right side of the equation.",
      "output": "8x = -29"
    },
    {
      "explanation": "Divide both sides by 8 to solve for x.",
      "output": "x = -29 / 8"
    },
    {
      "explanation": "Simplify -29 / 8, which is already in its simplest form as a fraction. We can also express it as a decimal.",
      "output": "x = -3.625"
    }
  ],
  "final_answer": "x = -3.625"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 3.2 XGrammar结构化生成引擎

XGrammar是由陈天奇团队推出的开源软件库,专为大型语言模型(LLM)设计,提供高效、灵活且可移植的结构化数据生成能力。它基于上下文无关语法(CFG)定义结构,支持递归组合以表示复杂结构,适合生成JSON、SQL等格式数据。

XGrammar通过字节级下推自动机优化解释CFG,减少每Token延迟,实现百倍加速,几乎无额外开销。此外,XGrammar集成多种系统优化,如自适应token掩码缓存、上下文扩展等,提高掩码生成速度并减少预处理时间。XGrammar的C++后端设计易于集成,并支持在LLM推理中实现零开销的结构化生成。

XGrammar

注:XGrammar已经集成到了vLLM大模型推理加速框架里了,详见:结构化输出 (opens new window)

# 3.3 Instructor控制大模型稳定输出

用于处理大语言模型结构化输出的 Python 库。它基于 Pydantic 实现了数据验证和类型注释,能够将 LLM 的结果转换为结构化数据,支持多种大语言模型服务,以及自动重试、流式响应等功能。

# 3.4 TypeChat控制大模型稳定输出

# 3.4.1 TypeChat简介

TypeChat 是微软开放的用于让大语言模型输出符合类型定义的稳定结果的库,可以将用户的自然语言输入转换为结构化的数据用于业务应用。TypeChat技术本质上是通过校验与重试机制使其最终稳定输出结构化数据。

TypeChat原理

# 3.4.2 TypeChat使用示例

[1] 准备运行环境

TypeChat 需要 Python 3.11环境才能运行,低版本的用不了,建议用 Conda 创建个独立的新环境。

$ git clone https://github.com/microsoft/TypeChat.git
$ conda create -n conda_typechat_env python=3.11
$ conda activate conda_typechat_env
$ pip3 install "typechat @ git+https://github.com/microsoft/TypeChat#subdirectory=python"
$ pip3 install python-dotenv
1
2
3
4
5

然后在根目录下新建 .env 文件,写入大模型 API 的连接信息,这里以 Deepseek 的为例:

OPENAI_MODEL="deepseek-chat"
OPENAI_API_KEY="sk-xxx"
OPENAI_ENDPOINT="https://api.deepseek.com/v1/chat/completions"
1
2
3

[2] 运行官方示例

./python/examples 目录下有官方的使用示例,详见:https://github.com/microsoft/TypeChat/tree/main/python/examples (opens new window)

TypeChat官方使用示例

./python/examples/sentiment 目录下的情绪分类为例:

demo.py

import asyncio

import sys
from dotenv import dotenv_values
import schema as sentiment
from typechat import Failure, TypeChatJsonTranslator, TypeChatValidator, create_language_model, process_requests

async def main():    
    env_vals = dotenv_values()
    model = create_language_model(env_vals)
    validator = TypeChatValidator(sentiment.Sentiment)
    translator = TypeChatJsonTranslator(model, validator, sentiment.Sentiment)

    async def request_handler(message: str):
        result = await translator.translate(message)
        if isinstance(result, Failure):
            print(result.message)
        else:
            result = result.value
            print(f"The sentiment is {result.sentiment}")

    file_path = sys.argv[1] if len(sys.argv) == 2 else None
    await process_requests("> ", file_path, request_handler)


if __name__ == "__main__":
    asyncio.run(main())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

schema.py

from dataclasses import dataclass
from typing_extensions import Literal, Annotated, Doc

@dataclass
class Sentiment:
    """
    The following is a schema definition for determining the sentiment of a some user input.
    """

    sentiment: Annotated[Literal["negative", "neutral", "positive"], Doc("The sentiment for the text")]
1
2
3
4
5
6
7
8
9
10

可以通过如下两种方式启动,后者是执行预先定义好的示例问题。

$ python3 python/examples/sentiment/demo.py                                         // 交互式启动
$ python3 python/examples/sentiment/demo.py python/examples/sentiment/input.txt     // 直接执行示例

>>> 运行结果:
> hello, world
The sentiment is neutral
> TypeChat is awesome!
The sentiment is positive
> I'm having a good day
The sentiment is positive
> it's very rainy outside
The sentiment is neutral
1
2
3
4
5
6
7
8
9
10
11
12

过程分析:这个库就是在将用户输入的prompt做了处理,然后对大模型的输出进行了校验,自带重试机制,见 ./python/src/typechat/_internal/model.py

class HttpxLanguageModel(TypeChatLanguageModel, AsyncContextManager):
    url: str
    headers: dict[str, str]
    default_params: dict[str, str]
    # Specifies the maximum number of retry attempts.
    max_retry_attempts: int = 3
    # Specifies the delay before retrying in milliseconds.
    retry_pause_seconds: float = 1.0
    # Specifies how long a request should wait in seconds
    # before timing out with a Failure.
    timeout_seconds = 10
    _async_client: httpx.AsyncClient
1
2
3
4
5
6
7
8
9
10
11
12

# 3.5 LangChain-Extract提取结构化数据

# 3.5.1 LangChain-Extract简介

LangChain-Extract是一个简单的 Web 服务器,允许使用 LLM 从文本和文件中提取信息。

# 3.5.2 搭建LangChain-Extract服务端

实验环境:Debian11、Docker 25.0.3、Docker Compose 2.6.0、8GB内存、160GB存储(该项目实际占用5G)

$ git clone https://github.com/langchain-ai/langchain-extract.git
$ cd langchain-extract
$ echo "OPENAI_API_KEY=API密钥" > .local.env
$ docker compose build
$ docker compose up
1
2
3
4
5

注意事项:最初我的 Docker 版本是20.10.17,构建镜像时出现如下报错,将其升级版本即可正常构建。

$ sudo apt-get install docker-ce docker-ce-cli containerd.io   // 升级Docker版本,原有镜像和容器还在,可能需要重启容器
$ docker version
1
2

langchain-extract构建镜像报错

# 3.5.3 使用LangChain-Extract提取数据

Step1:创建一个用户ID

$ USER_ID=$(python3 -c 'import uuid; print(uuid.uuid4())')
$ echo $USER_ID
输出内容:
958dde00-3db4-4e0a-bae8-592135387a20
$ export USER_ID
1
2
3
4
5

注:官方使用的是 USER_ID=$(uuidgen) 命令来生成,我没安装这个库,就用python的uuid库去生成了,直接手动写死也行。如果这里没提前设置过,后面执行创建提取器的命令会报错{"detail":"Not authenticated"}

Step2:创建一个提取器

$ curl -X 'POST' \
  'http://localhost:8000/extractors' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -H "x-key: ${USER_ID}" \
  -d '{
  "name": "案件内容",
  "description": "用于提取案件内容主要信息",
  "schema": {
      "type": "object",
      "title": "案件",
      "required": [
        "案件名称",
        "被请求人",
        "执法依据",
        "违法金额"
      ],
      "properties": {
        "案件名称": {
          "type": "string",
          "title": "案件名称"
        },
        "被请求人": {
          "type": "string",
          "title": "被请求人"
        },
        "执法依据": {
          "type": "string",
          "title": "执法依据"
        },
        "违法金额": {
          "type": "integer",
          "title": "违法金额"
        }
      }
    },
  "instruction": "使用给定输入中的有关该案件内容的信息"
}'

输出内容:
{"uuid":"07b6e509-e93a-4f27-820b-ec742511c570"}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

注:-H "x-key: ${USER_ID}" 这里是不需要修改的,只修改定义提取器的JSON即可。

Step3:使用该提取器从文本中提取数据

$ curl -s -X 'POST' \
'http://localhost:8000/extract' \
-H 'accept: application/json' \
-H 'Content-Type: multipart/form-data' \
-H "x-key: ${USER_ID}" \
-F 'extractor_id=07b6e509-e93a-4f27-820b-ec742511c570' \
-F 'text=当事人王振在未经登记机关核准登记取得营业执照的情况下,擅自于2020年11月23日起在诸暨市枫桥镇从事机油经营活动,于2020年11月24日被本局查获。另查明:当事人王振在2020年11月23日购进二箱嘉实多磁护全合成机油(5W-40),每箱六瓶,每瓶净含量4升,生产日期均为20200812,共计12瓶,纸箱及瓶身上标有“Castrol”、“嘉实多”商标,进价为150元/瓶,2020年11月24日带到枫桥准备出售,销售价200元/瓶,合计违法经营额2400元。上述全合成机油均未出售。于2020年11月24日被本局查获。本局对上述尚未出售的2箱“Castrol”“嘉实多”商标的全合成机油予以扣押。至查获时止,共计违法经营额2400元。另查明:“Castrol”商标是经国家市场监督管理总局核准注册的注册商标。注册号:1372240;注册人:嘉实多有限公司(CASTROL LIMITED);注册人地址:英国RG8 7QR瑞丁庞伯恩惠特彻奇山技术中心;核定使用商品 第4类:核定使用商品:工业用油; 工业用脂; 润滑剂; 润滑油及脂; 燃料; 汽车燃料非化学添加剂; 润滑油及油脂; 吸尘、润湿和除尘粘合用合成制剂; 照明用油脂; 蜡烛; 工业用蜡等。注册有效期限:2011年11月28日至2021年11月27日。 “嘉实多”商标是经国家市场监督管理总局核准注册的注册商标。注册号:832054;注册人:嘉实多有限公司(CASTROL LIMITED);注册人地址:英国RG8 7QR瑞丁庞伯恩惠特彻奇山技术中心;核定使用商品 第4类:核定使用商品:工业用油及油脂; 润滑剂; 泣脂; 燃料; 润滑剂和油脂用非化学添加剂; 除尘; 喷酒和粘结灰尘剂; 照明燃料; 齿轮油; 传动油(工业用)等。注册有效期限:至2026年04月20日。当事人销售的全合成机油使用的“Castrol”、“嘉实多”商标标识与上述注册商标相同。经商标注册人的受权人嘉实多(深圳)有限公司鉴定,上述全合成机油系假冒商品,侵犯了商标注册人的商标专用权。经查明当事人的上述无照经营行为违反了《无证无照经营查处办法》第二条“任何单位或者个人不得违反法律、法规、国务院决定的规定,从事无证无照经营。”之规定,属无照经营行为,责令改正。根据《中华人民共和国商标法》第六十条第二款:“工商行政管理部门处理时,认定侵权行为成立的,责令立即停止侵权行为,没收、销毁侵权商品和主要用于制造侵权商品、伪造注册商标标识的工具,违法经营额 五万元以上的,可以处违法经营额五倍以下的罚款,没有违法经营额或者违法经营额不足五万元的,可以处二十五万元以下的罚款。对五年内实施两次以上商标侵权行为或者有其他严重情节的,应当从重处罚。”之规定,建议责令停止侵权行为并处罚如下:一:没收扣押的标注有“Castrol”、“嘉实多”商标的全合成机油2箱共12瓶; 二:罚款3500元' \
-F 'mode=entire_document' \
-F 'file=' | jq .

输出内容:
{
  "data": [
    {
      "执法依据": "未经登记机关核准登记取得营业执照",
      "案件名称": "非法经营机油案",
      "被请求人": "王振",
      "违法金额": 2400
    }
  ],
  "content_too_long": false
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

注:需要把 extractor_id 的字段信息改成上一步输出的提取器 uuid。

# 4. 参考资料

[1] LangChain Introduction from LangChain官方文档 (opens new window)

[2] LangChain 中文入门教程 from Gitbook (opens new window)

[3] LangChain:一个让你的LLM变得更强大的开源框架 from 知乎 (opens new window)

[4] 500页超详细中文文档教程,助力LLM/chatGPT应用开发 from LangChain中文网 (opens new window)

[5] OpenAI Q&A: Finetuning GPT-3 vs Semantic Search - which to use, when, and why? from YouTube (opens new window)

[6] langfow:0门槛0代码拖拽生成企业的专属Al大模型,专属llm会成为每个企业的标配 from Bilibili (opens new window)

[7] 本地部署开源大模型的完整教程:LangChain + Streamlit+ Llama from 知乎 (opens new window)

[8] LangFlow--可视化的LangChain from 土猛的员外 (opens new window)

[9] 500页超详细中文文档教程,助力LLM/chatGPT应用开发 from LangChain中文网 (opens new window)

[10] 用LangChain构建大语言模型应用 from CSDN (opens new window)

[11] Awesome LangChain from Github (opens new window)

[12] 一图带你了解 LangChain 的功能模块 from Bilibili (opens new window)

[13] LangChain结合了大型语言模型、知识库和计算逻辑,可以用于快速开发强大的AI应用 from Github (opens new window)

[14] 通过Typechat控制LLM的输出 from 知乎 (opens new window)

[15] AI 调教师:聊聊 TypeChat 以及ChatGPT 形式化输出 from 腾讯云 (opens new window)

[16] TypeChat 全面指南:从核心概念到使用 from InfoQ (opens new window)

[17] OpenAI 在 API 中引入 JSON 结构化输出功能 from oschina (opens new window)

[18] 卡了大模型脖子的Json输出,OpenAI终于做到了100%正确 from 微信公众号 (opens new window)

[19] 结构化输出功能,OpenAI是如何实现的 from 知乎 (opens new window)

[20] 千呼万唤终于来了,OpenAI的API终于能够百分之百输出JSON了 from 微信公众号 (opens new window)

[21] XGrammar:陈天奇团队推出的LLM结构化生成引擎 from CSDN (opens new window)

[22] 首个提升大模型工作流编排能力的大规模数据集开源 from 微信公众号 (opens new window)

Last Updated: 3/15/2025, 10:55:15 AM