贫瘠之地

华北无浪漫,死海扬起帆
多少个夜晚,独自望着天

0%

LLM 信息提取结构化输出

背景

看了新一期的阮一峰周刊,引用了一篇博客 Information Extraction with Large Language Models - Parsing Unstructured Data with GPT-3

Information Extraction with Large Language Models - Parsing Unstructured Data with GPT-3 (marcotm.com)

In the past months, ChatGPT has been dominating the news headlines, and people are both excited and scared by its quite sophisticated ability to generate texts. Besides short- and long-form text generation, there are quite a few other use cases which provide a lot of practical value. With the current generation of these large language models (LLMs), many of the classic tasks in Natural Language Processing (NLP) such as text classification, sentiment analysis, or named entity recognition, are almost trivial to solve.

在过去的几个月里,ChatGPT 一直占据着新闻头条,人们对它相当复杂的文本生成能力既兴奋又害怕,除了生成短格式和长格式文本外,还有许多其他用例提供了很大的实用价值

随着这些大型语言模型(LLM)的出现,自然语言处理(NLP)中的许多经典任务,如文本分类、情感分析或命名实体识别,几乎都很难解决

In this article, I have documented some experimentation with how to use GPT-3 (update: and 3.5) to extract structured information from unstructured texts and I hope the article can serve as a tutorial for how to approach such a task with an LLM.

在这篇文章中,我记录了一些关于如何使用 GPT-3(更新:和 3.5)从非结构化文本中提取结构化信息的实验,我希望这篇文章可以作为如何使用 LLM 处理此类任务的教程

作者维护了一个招聘网站,但是招聘信息是以非结构化文本形式进行投递,作者希望将其重要信息提取出来,维护数据后用户可以通过相关性进行查询

With about 500 job posts per month, some of them advertising multiple roles, we end up with over 1000 jobs per month. As you can see on the site, there are several filters such as "remote", "part-time", etc. which allow you to narrow down the choices based on some general characteristics.

每月约有 500 个招聘信息,其中一些信息包含多个职位的内容,即我们最终每月有 1000 多个职位

正如你在网站上看到的,有几个过滤器,如“远程”、“兼职”等,允许你根据一些一般特征缩小选择范围

However, a job board usually also offers to select a certain job category (e.g., "iOS developer"). Since the "Who is hiring" threads are not limited to certain types of jobs and often there are interesting roles that might not fit into the usual categories, I tried a different approach for how to sort the jobs according to one's interests: For each job, a text embedding for the job description (including the company description) is created, which then can be used to sort by similarity to a selected job.

然而,招聘委员会通常也会提供选择特定的工作类别(例如,“iOS开发者”)

由于“谁在招聘”主题并不局限于某些类型的工作,而且通常有一些有趣的角色可能不属于通常的类别,我尝试了一种不同的方法来根据个人的兴趣对工作进行排序:对于每个工作,都会创建一个 embedding 工作描述(包括公司描述)的文本,其然后可以用于根据与所选作业的相似性进行排序

这里就来体验下作者对于 LLM(GPT-3.5)解析文本输出结构化信息的 prompt,以及尝试使用 LangChain 进行实现

基本思路

以下是作者制作出这一 Prompt 的主要想法和观察结果:

  • 提示首先描述基本任务 turn unstructured job posting [..] into a JSON
  • 提醒一则招聘信息可以包括多个角色或职位
  • 详细描述了所需的输出格式;对于每个字段,指定字段名称和字段类型
  • 对于某些字段给出了额外的提示,这些提示大多来自观察到的错误案例或不良行为;例如,角色有时以单数形式书写,有时以复数形式书写,原始提示逐字复制,但希望 LLM 可以将其标准化
  • 让 LLM 清楚如果没有明确信息,可以将字段设置为 null
  • 在以前的版本中,在某些情况下职位会被列出两次,没有任何差异;原因是该招聘信息内容为该公司正在寻找两名开发,因此添加了指令,只包含了一次完全解决问题的需求
  • 在说明之后,添加原始的招聘信息
  • 然后声明输出使用 JSON 格式
  • 在看到的一个技巧是用 ```json,这是一些标记语言在文档中包含代码和类似文本的典型方式
  • 最后还编写了应该完成的实际 JSON 的第一部分

参数

作者认为对于这种任务,不希望模型变得有创造性

因此使用了以下参数:

  • Temperature:0
  • Maximum:4000 -
  • Stop sequences:```
  • Top P:0.1
  • Frequency penalty:0
  • Presence penalty:0

Prompt

文章中作者说明了 GPT-3.5 是基于聊天的模型,所以对 Prompt 进行了一些微调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Your task is to parse an unstructured job posting and turn it into a JSON containing the most important information. The job posting can describe one or more jobs at the same company. The JSON should consist of the following information:
- The company name (field name: "companyName", field type: string)
- the location of the company (field name: "companyLocation", field type: string); if not explictily stated, you can try to infer the company's actual location from other clues, e.g., something like "Remote (US)" usually means that the company is located in the US; if the location cannot be inferred, set it to null
- a short description of what the company is doing or building (field name: "companyDescription", field type: string); try to keep it short (max length: ca. 300 characters)
- a list of advertised jobs (field name: "jobs", field type: array).
Each element of the "jobs" array should contain the following fields:
- The job title (field name: "jobTitle", field type: string); the job title should be given in the singular form (i.e., Frontend Developer instead of Frontend Developers)
- the salary range (field name: "salary", field type: string); only include explictly stated salary amounts, otherwise set to null
- whether equity is part of the compensation (field name: "equity", field type: boolean)
- the benefits (field name: "benefits", field type: string); include things like 401k, insurance, equipment, child care, etc. if stated, otherwise set to null
- the location of the job (field name: "location", field type: string)
- whether this is a job for senior/experienced candidates (field name: "senior", field type: boolean); typically senior, staff, lead, principal, vp, cto, etc. positions are all regarded as senior level
- whether it is a remote opportunity (field name: "remote", field type: boolean)
- whether it can be done onsite from an office (field name: "onsite", field type: boolean)
- whether it can be done part-time (field name: "partTime", field type: boolean)
- whether it can be done full-time (field name: "fullTime", field type: boolean)
- the URL to the specific job description (field name: "jobUrl", field type: string)
- and any specific requirements/skills that might be stated (field name: "requirements", field type: string).
In general, if certain information is not stated, set the respective field to null. If the company seeks more than one person for the same role, include the role only once. Please output only the pure JSON representation. Do not include any explanations, comments, thoughts, etc. The output has to be a valid JSON object which can be parsed as is.

This is the job posting:

%s

因为感觉 Azure 对于中文的支持比较好,这里我调整成了中文,并且使用 LangChain 工具进行 Prompt 的组装

Prompt 组装

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
template = """您的任务是解析一个非结构化的招聘信息,并将其转换为包含最重要信息的 JSON
职位发布可以描述同一公司的一个或多个职位;JSON 应包含以下信息
- 公司名称(字段名称:companyName,字段类型:字符串)
- 公司的位置(字段名称:companyLocation,字段类型:字符串);如果没有明确说明,你可以尝试从其他线索推断公司的实际位置,例如,像“远程(美国)”这样的词通常意味着公司位于美国;如果无法推断位置,请将其设置为 null
- 公司正在做什么或正在建设什么的简短描述(字段名:companyDescription,字段类型:字符串);尽量保持短(最大长度:约 300 个字符)
- 招聘广告的职位清单(字段名:jobs,字段类型:数组)
jobs 数组的每个元素都应包含以下字段:
- 职务(字段名称:jobTitle,字段类型:字符串);职位名称应以单数形式给出(即,前端开发人员而非前端开发人员)
- 薪资范围(字段名称:salary,字段类型:字符串);仅包括明确说明的工资金额,否则设置为 null
- 股权是否是补偿的一部分(字段名称:equity,字段类型:布尔值)
- 公司福利(字段名称:benefits,字段类型:字符串);包括养老金、保险、设备、儿童保育、年假等,如果没有说明则设置为 null
- 工作地点(字段名:location,字段类型:字符串)
- 这是否是一份适合资深/有经验的候选人的工作(字段名称:senior,字段类型:布尔值);通常,高级、职员、领导、校长、副总裁、首席技术官等职位都被视为高级职位
- 是否为远程办公(字段名称:remote,字段类型:布尔值)
- 是否可以兼职(字段名:partTime,字段类型:布尔值)
- 特定作业描述的URL(字段名:jobUrl,字段类型:字符串)
- 以及可能说明的任何特定要求/技能(字段名称:requirements,字段类型:字符串)
通常,如果没有说明某些信息,请将相应的字段设置为 null
如果公司为同一职位寻找多个人,则只包括该职位一次
请仅输出纯 JSON 表示
不包括任何解释、评论、想法等
输出必须是一个有效的 JSON 对象,可以按原样解析
"""
system_message_prompt = SystemMessagePromptTemplate.from_template(template)
human_template = "这是招聘信息:\n{text}"
human_message_prompt = HumanMessagePromptTemplate.from_template(human_template)

招聘信息存储在 txt 文件

这里直接使用了阮一峰博客“谁在招人”广告中的招聘信息;具体内容可以查看

谁在招人?(2023年12月) · Issue #3684 · ruanyf/weekly (github.com)

这里随便选取一个

1
2
3
4
5
6
7
8
9
10
【杭州】网易云音乐
有意向的同学可联系 semanwmj@gmail.com

资深前端开发工程师
职位描述
负责云音乐社交直播的前端开发工作。
参与各类型项目(Web&Webview、中后台系统、混合应用、小程序、创意活动及游戏)的前端开发工作,完成系统设计、技术选型、模块开发工作。
以不断提升质量、效率、体验为目的,封装组件、沉淀文档、生产工具、搭建系统, 享受工程师的日常。
分享自己日常的所见所想,引导同事共同成长,营造积极健康的技术氛围。
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 组装 chain
chain = LLMChain(
llm=chat_model,
prompt=chat_prompt
)

# 读取文件
file_path = "../content.txt"
with open(file_path, "r", encoding="utf-8") as file:
content = file.read()

# 执行
result = chain.run(content)
print(result)

结果

LLM 输出的 JSON 格式的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"companyName": "网易云音乐",
"companyLocation": "杭州",
"companyDescription": "网易云音乐是一家位于杭州的音乐公司,专注于社交直播和音乐领域的前端开发工作。",
"jobs": [
{
"jobTitle": "资深前端开发工程师",
"salary": null,
"equity": false,
"benefits": null,
"location": "杭州",
"senior": true,
"remote": false,
"partTime": false,
"jobUrl": null,
"requirements": "精通各种 Web 前端技术和标准(Javascript、HTML、CSS),熟悉页面布局常用解决方案,对表现与数据分离、Web语义化等有深刻理解。熟练掌握一个数据驱动视图的框架( React、Sevlte、Solid、Vue)。熟练掌握常用的前端构建工具(Webpack、Vite 等),并具有成熟的模块化开发思维。熟悉常用前端数据管理的解决方案(如 Redux、Zustand、Jotai 等),并清楚它们的优劣和应用场景。熟悉 HTTP 协议,并掌握相关网络调试工具,有服务端开发的背景常识。对重复性或不规范的工作容忍度低,能自发通 过沟通或技术手段解决。"
},
...
]
}

LangChain Pydantic 解析器

上面已经使用 LangChain 管理了 LLM、Prompt,不如直接应用 Pydantic 解析器来直接解析成结构化的数据

好处是定义好结构后,LangChain 默认可以生成一套对于结构化输出的提示,即 get_format_instructions 方法

结构定义

这里使用 Pydantic 定义结构,对应上面的 Prompt

  • 字段名、数据类型进行对应,其中 jobs 的类型为新定义的 Job 结构
  • 描述中拆分出 title 和 description;对于无法定义出区别的字段,就使用同样的值(例如 股权是否是补偿的一部分
  • 为了省事,所有的字段都声明了允许为 None;其实这个可以结合实际需求进行强校验,不通过往往说明招聘信息内容确实(例如换公司名称、公司位置等应该是必填项)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Job(BaseModel):
jobTitle: str = Field(None, title="职务", description="职位名称应以单数形式给出(即,前端开发人员而非前端开发人员)")
salary: str = Field(None, title="薪资范围", description="仅包括明确说明的工资金额,否则设置为 null")
equity: bool = Field(None, title="股权是否是补偿的一部分", description="股权是否是补偿的一部分")
benefits: str = Field(None, title="公司福利", description="包括养老金、保险、设备、儿童保育、年假等,如果没有说明则设置为 null")
location: str = Field(None, title="工作地点", description="工作地点", )
senior: bool = Field(None, title="这是否是一份适合资深/有经验的候选人的工作", description="这是否是一份适合资深/有经验的候选人的工作")
remote: bool = Field(None, title="是否为远程办公", description="是否为远程办公")
partTime: bool = Field(None, title="是否可以兼职", description="是否可以兼职")
jobUrl: str = Field(None, title="特定作业描述的URL", description="特定作业描述的URL", )
requirements: str = Field(None, title="特定要求", description="特定要求", )


class Info(BaseModel):
companyName: str = Field(None, title="公司名称", description="公司名称")
companyLocation: str = Field(None, title="公司位置", description="如果没有明确说明,你可以尝试从其他线索推断公司的实际位置,例如,像“远程(美国)”这样的词通常意味着公司位于美国;如果无法推断位置,请将其设置为 null")
companyDescription: str = Field(None, title="公司描述", description="公司正在做什么或正在建设什么的简短描述,尽量保持短")
jobs: List[Job] = Field(None, title="职位清单", description="招聘广告的职位清单")

创建解析器和 Prompt

1
2
3
4
5
6
7
8
9
10
11
12
# Pydantic 解析器
pydantic_parser = PydanticOutputParser(pydantic_object=Info)

# get_format_instructions 用于获取生成的提示
format_instructions = pydantic_parser.get_format_instructions()

# 将额外说明放在 Prompt 中
prompt = PromptTemplate(
template="尽可能的将招聘信息解析为 JSON 格式\n通常,如果没有说明某些信息,请将相应的字段设置为 null;如果公司为同一职位寻找多个人,则只包括该职位一次\n请仅输出纯 JSON 表示。不包括任何解释、评论、想法等。输出必须是一个有效的 JSON 对象,可以按原样解析\n{format_instructions}\n{text}",
input_variables=["text"],
partial_variables={"format_instructions": format_instructions}
)

执行

执行同上,但是在 chain 中入参结果解析器

1
2
3
4
5
6
7
8
9
10
11
12
13
# output_parser 参数指定结果解析器
chain = LLMChain(
llm=chat_model,
prompt=prompt,
output_parser=pydantic_parser
)

file_path = "D:/WorkSpace_learn/langchain-test/text.txt"
with open(file_path, "r", encoding="utf-8") as file:
content = file.read()

result = chain.run(content)
print(result)

结果

1
companyName='Apifox' companyLocation='天河 CBD' companyDescription='Apifox 是 API 文档、API 调试、API Mock、API 自动化测试一体化协作平台' jobs=[Job(jobTitle='初中级前端开发工程师', salary='12~25K', equity=None, benefits=None, location=None, senior=False, remote=False, partTime=False, jobUrl=None, requirements='本科及以上学历,至少 1 年以上 web 前端开发工作经验; 精通使用各种 web 前端技术,包括 HTML5/CSS/Javascript/TypeScript 等 ; 熟悉一种或多种常用前端框架,如( React 、Vue 、Angular 等); 熟悉使用 webpack 等模块化、工程化工具; 拥 有开发流程中的代码规范意识、配置管理规范意识、文档撰写规范意识;'), Job(jobTitle='高级前端开发工程师', salary='20~40K', equity=None, benefits=None, location=None, senior=True, remote=False, partTime=False, jobUrl=None, requirements='本科及以上学历,至少 1 年以上 web 前端开发工作经验; 精通使用各种 web 前端技术,包括 HTML5/CSS/Javascript/TypeScript 等; 熟悉一种或多种常用前端框架,如( React 、Vue 、Angular 等); 熟悉使用 webpack 等模块化、工程化工具; 拥有开发流程中的代码规范意识、配置管理规范意识、文档撰写规范意识;'), Job(jobTitle='专家前端开发工程师', salary='35~60K', equity=None, benefits=None, location=None, senior=True, remote=False, partTime=False, jobUrl=None, requirements='本科及以上学历,至少 1 年以上 web 前端开发工作经验; 精通使用各 种 web 前端技术,包括 HTML5/CSS/Javascript/TypeScript 等; 熟悉一种或多种常用前端框架,如( React 、Vue 、Angular 等); 熟悉使用 webpack 等模块化、工程化工具; 拥有开发流程中的代码规范意识、配置管理规范意识、文档撰 写规范意识;')]

思考

将文本解析为结构化数据带来的好处:

  • 效率和成本:减少人工处理的工作量,提高数据处理的效率,降低数据处理的成本
  • 正确性:(也许?)避免了人为错误的可能性
  • 增强数据可用性:将文本转化为结构化数据后,数据变得更易于管理和分析
  • 支持决策制定: 结构化数据使得信息更容易理解和比较,为决策制定提供更多支持

文中的需求一大特点就是用于增强数据的可用性

其次我认为是不是可以用来作为自然语言和 AI 沟通的桥梁;比如一个 SQL 需求, “查询学生姓名和年龄,按照成绩排序”

第一步先使用结构引导 LLM 对需求进行拆解,假设使用 JSON 结构

1
2
3
4
5
{
"table": "学生",
"filed": ["姓名","年龄"],
"order": "成绩"
}

第二步再以结构化数据驱动 LLM 转换为 SQL 结构,是不是可以更好地引导 LLM 对需求进行理解(我胡乱想的)