一、问题和需求
需求:上传一个文件,不论是PDF还是图片还是docx。上传到AI服务端。AI服务端可以让大模型返回指定的json信息。实现文件信息智能抽取的功能。
出现下面问题:
1.怎么把各种各样的文件快速高效转化成文本信息或者md格式的文本信息?
这个问题我已经通过Umi-OCR和MarkltDown共同实现了一个多文档格式处理器类。可以实现对各种文件的快速转文本或MD格式文本的处理。
Umi-OCR,完美解决企业OCR的核心痛点!!!【史上最全(万字)安装加测评】-CSDN博客
MarkItDown如何接入AI系统提供服务?-CSDN博客
2. 得到了文件的文本信息后,如何使用langchain框架让大模型结构化输出信息?
3. 怎么写好提示词让模型输出更符合我们的希望?
4. 如果大模型的抽取效果不好,怎么排查原因和优化?
二、Pydantic
2.1 Pydantic是什么
Pydantic 是一个强大的数据验证库,通过 Python 类型提示提供了简洁而强大的方式来定义和验证数据结构。它的 BaseModel
类是创建数据模型的基础,提供了自动类型转换、数据验证、序列化等功能。Pydantic 在现代 Python 开发中越来越受欢迎,特别是在 API 开发和数据处理的场景中。
三、langchain里抽取结构化信息的四种方法
3.1 四种方法的代码测试
import time
from get_model.get_llm import get_api_llm, get_local_llm
from configs.basic_configs import MODEL_PATH, MODEL_URL
from langchain.prompts import PromptTemplate
from langchain.output_parsers import StructuredOutputParser, ResponseSchema, PydanticOutputParser
from langchain_core.output_parsers import StrOutputParser
from pydantic import BaseModel, Field
# 导入模型
llm = get_local_llm(MODEL_PATH=MODEL_PATH, MODEL_URL=MODEL_URL)
def method1_pydantic_structured(query: str):
"""方法一: 使用自定义的pydantic类 + with_structured_output"""
start_time = time.time()
class hb_Date(BaseModel):
year: int = Field(description="Year")
month: int = Field(description="Month")
day: int = Field(description="Day")
structured_llm = llm.with_structured_output(hb_Date)
template = """
提取用户输入中的日期。
用户输入:
{query}
"""
prompt = PromptTemplate(template=template)
response = structured_llm.invoke(query)
result = response.model_dump() if hasattr(response, 'model_dump') else response.dict()
end_time = time.time()
return result, end_time - start_time
def method2_json_schema_structured(query: str):
"""方法二:使用json_schema + with_structured_output"""
start_time = time.time()
json_schema = {
"title": "Date",
"description": "Formated date expression",
"type": "object",
"properties": {
"year": {
"type": "integer",
"description": "年份, YYYY",
},
"month": {
"type": "integer",
"description": "月份, MM",
},
"day": {
"type": "integer",
"description": "日期, DD",
},
},
}
structured_llm = llm.with_structured_output(json_schema)
template = """
务必准确提取用户输入中的日期。
用户输入:
{query}
"""
prompt = PromptTemplate(template=template)
response = structured_llm.invoke(prompt.format_prompt(query=query))
end_time = time.time()
return response, end_time - start_time
def method3_structured_parser(query: str):
"""方法三:使用StructuredOutputParser类"""
start_time = time.time()
response_schemas = [
ResponseSchema(name="year", description="年份"),
ResponseSchema(name="month", description="月份"),
ResponseSchema(name="day", description="日期")
]
output_parser = StructuredOutputParser.from_response_schemas(response_schemas)
format_instructions = output_parser.get_format_instructions()
template = """
请根据以下要求生成输出,确保输出为严格的 JSON 格式,且必须只包含以下字段:
{format_instructions}
不要使用 Markdown 代码块包裹 JSON!
文本:
{query}
"""
prompt = PromptTemplate(
template=template,
input_variables=["query"],
partial_variables={"format_instructions": format_instructions}
)
chain = prompt | llm | StrOutputParser() | output_parser
result = chain.invoke({"query": query})
end_time = time.time()
return result, end_time - start_time
def method4_pydantic_parser(query: str):
"""方法四:使用 PydanticOutputParser类"""
start_time = time.time()
class Date(BaseModel):
year: int = Field(description="年份")
month: int = Field(description="月份")
day: int = Field(description="日期")
parser = PydanticOutputParser(pydantic_object=Date)
format_instructions = parser.get_format_instructions()
template = """
请阅读下面的文本,并生成一个严格的 JSON 对象,不要使用 Markdown 代码块包裹!
格式要求:
{format_instructions}
文本:
{query}
"""
prompt = PromptTemplate(
template=template,
input_variables=["query"],
partial_variables={"format_instructions": format_instructions}
)
chain = prompt | llm | StrOutputParser() | parser
result = chain.invoke({"query": query})
end_time = time.time()
return result.dict(), end_time - start_time
def test_methods():
"""测试所有方法的效果和速度"""
test_query = """今天是二〇二五年二月二十日天气晴,遇到很多人。
这是模型设计支持的最大上下文长度(即模型能同时处理的文本最大长度),单位为 token(约等于512个汉字或700个英文单词)。
正常情况:当输入文本的 token 数量 ≤ 512 时,模型会正常处理且不会报错。
超出限制时:若输入文本 token 数量 > 512,具体行为取决于调用方式:
部分框架(如 Transformers)会自动截断超长文本,仍能运行但丢失部分信息。
部分服务(如未配置截断)会触发 ContextExceededError类错误(如报错提示:Input length exceeds max context size)。
图中信息的佐证
"""
methods = [
("Pydantic类 + with_structured_output", method1_pydantic_structured),
("JSON Schema + with_structured_output", method2_json_schema_structured),
("StructuredOutputParser类", method3_structured_parser),
("PydanticOutputParser类", method4_pydantic_parser)
]
results = []
for name, method in methods:
try:
result, time_cost = method(test_query)
results.append({
"method": name,
"result": result,
"time_cost": time_cost,
"success": True
})
except Exception as e:
results.append({
"method": name,
"error": str(e),
"success": False
})
# 打印测试结果
print("\n=== 测试结果 ===")
for r in results:
print(f"\n方法: {r['method']}")
if r['success']:
print(f"结果: {r['result']}")
print(f"耗时: {r['time_cost']:.2f}秒")
else:
print(f"错误: {r['error']}")
if __name__ == "__main__":
test_methods()
3.2 四种方法的说明
方法 | 开发效率 | 数据可靠性 | 性能 | 适用模型范围 |
---|---|---|---|---|
Pydantic + | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 支持原生结构化的模型 |
JSON Schema + | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 支持原生结构化的模型 |
| ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | 所有模型 |
| ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | 所有模型 |
方法一和方法二,需要模型本身支持结构化输出才可以使用,按道理来说速度最快。
支持结构化输出的模型有OpenAI系列和Qwen3系列【非思考模式下】的模型。
比如我通过API调用qwen-max,就会出现下面的结果。
如果自己写JSON Schema,效果不如pydantic。
总结:如果模型支持结构化输出,选择方法一。如果模型不支持结构化输出,选择方法四。方法三和方法二没有使用pydantic做数据校验。最好还是别用。
四、如何写更复杂的信息抽取类
4.1 需求
构建一个嵌套的输出的类,比如我需要最后输出的是,时间(包括年、月、日),地点,人物(姓名、性别、国籍、年龄)。如果文本信息没有这种内容就输出空字符串。
4.2 实现代码
# 如果模型支持结构化输出,选择方法一。如果模型不支持结构化输出,选择方法二。
import json
import time
from get_model.get_llm import get_api_llm, get_local_llm
from configs.basic_configs import MODEL_PATH, MODEL_URL
from langchain.prompts import PromptTemplate
from langchain.output_parsers import StructuredOutputParser, ResponseSchema, PydanticOutputParser
from langchain_core.output_parsers import StrOutputParser
from pydantic import BaseModel, Field
# 导入模型
# llm = get_local_llm(MODEL_PATH=MODEL_PATH, MODEL_URL=MODEL_URL)
llm = get_api_llm()
print(llm.invoke("你好"))
def method1_pydantic_structured(query: str):
"""方法一: 使用自定义的pydantic类 + with_structured_output"""
start_time = time.time()
class hb_Date(BaseModel):
year: int = Field(description="Year")
month: int = Field(description="Month")
day: int = Field(description="Day")
structured_llm = llm.with_structured_output(hb_Date)
template = """
提取用户输入中的日期。
用户输入:
{query}
"""
prompt = PromptTemplate(template=template)
response = structured_llm.invoke(query)
result = response.model_dump() if hasattr(response, 'model_dump') else response.dict()
end_time = time.time()
return result, end_time - start_time
def method2_pydantic_parser(query: str):
"""方法二:使用 PydanticOutputParser类"""
start_time = time.time()
class TimeInfo(BaseModel):
year: int = Field(description="年份,整数,如果没有则为空字符串")
month: int = Field(description="月份,整数,如果没有则为空字符串")
day: int = Field(description="日期,整数,如果没有则为空字符串")
class PersonInfo(BaseModel):
name: str = Field(description="姓名,如果没有则为空字符串")
gender: str = Field(description="性别,如果没有则为空字符串")
nationality: str = Field(description="国籍,如果没有则为空字符串")
age: str = Field(description="年龄,如果没有则为空字符串")
class ExtractedInfo(BaseModel):
time: TimeInfo = Field(description="时间信息")
location: str = Field(description="地点,如果没有则为空字符串")
person: PersonInfo = Field(description="人物信息")
parser = PydanticOutputParser(pydantic_object=ExtractedInfo)
format_instructions = parser.get_format_instructions()
template = """
请阅读下面的文本,提取时间、地点和人物信息。如果某项信息不存在,请使用空字符串。
生成一个严格的 JSON 对象,不要使用 Markdown 代码块包裹!
格式要求:
{format_instructions}
文本:
{query}
"""
prompt = PromptTemplate(
template=template,
input_variables=["query"],
partial_variables={"format_instructions": format_instructions}
)
chain = prompt | llm | StrOutputParser() | parser
result = chain.invoke({"query": query})
end_time = time.time()
return result.dict(), end_time - start_time
def test_methods():
"""测试所有方法的效果和速度"""
test_query = """今天是二〇二五年二月二十日天气晴,在北京遇到很多人。
其中包括来自美国的John,他是一位25岁的男性。这个城市非常美丽,有很多历史建筑。
"""
methods = [
("Pydantic类 + with_structured_output", method1_pydantic_structured),
("PydanticOutputParser类", method2_pydantic_parser)
]
results = []
for name, method in methods:
try:
result, time_cost = method(test_query)
results.append({
"method": name,
"result": result,
"time_cost": time_cost,
"success": True
})
except Exception as e:
results.append({
"method": name,
"error": str(e),
"success": False
})
# 打印测试结果
print("\n=== 测试结果 ===")
for r in results:
print(f"\n方法: {r['method']}")
if r['success']:
print("结果:")
# 使用json.dumps美化输出,ensure_ascii=False确保中文正常显示,indent=2设置缩进
print(json.dumps(r['result'], ensure_ascii=False, indent=2))
print(f"耗时: {r['time_cost']:.2f}秒")
else:
print(f"错误: {r['error']}")
if __name__ == "__main__":
test_methods()
只需要在最后的类中定义的时候,定义的类型又是自己定义过的就行。
4.3 效果如下
使用的是阿里api的qwen-flash模型
五、如何优化速度
5.1 实际生产问题
如果我们定义的pydantic模型类嵌套3层4层,比较复杂,模型的输出速度会慢很多。而且是越大参数的模型越慢。此时如何解决速度
文本信息结构化输出的,速度主要和两个因素有关:
- Pydantic模型类的嵌套层数和总字符数。
- 调用的模型如果是”回答效果”更好的话,耗时越多。
- 经过测试,和Pydantic模型类的嵌套层数有一点关系。但是还是主要和pydantic模型类整体的总字符数有关。
5.2 怎么解决
思路:减少pydantic模型类的复杂度、换“简单”一点的模型抽取信息
这边提供我的思路:
1.先用正则表达式把可以提取的信息提取出来,然后把文本信息简化、需要用大模型抽取的信息量就变少了,最后拼接到一起。