news 2026/10/15 4:41:00

[实践]-用LLM对文本结构化:把多段文本描述变成规范的 JSON 知识库

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
[实践]-用LLM对文本结构化:把多段文本描述变成规范的 JSON 知识库

本次实践以一个真实的推荐系统数据工程场景为背景:把 210 段风格自由、毫无 schema 可言的餐厅描述文本,批量转换成机器可查询的结构化 JSON 知识库。完整走一遍One-shot 提示抽取 → Pydantic 模式验证 → 错误回喂自动修复的三段式管道,最终 210 条输入产出 209 条合法 JSON——看看 LLM 结构化输出的工程化闭环到底怎么做,以及"让 LLM 自己修自己"这个看似偷懒的设计,为什么在工程上反而说得通。

一、问题:非结构化文本是搜索的天敌

场景不复杂:一家 AI 公司要做美食推荐产品Connoisseur Companion,不满足于传统的 1–5 星评分,想从评论文本里挖出餐厅的氛围、饮食限制和"招牌菜"。手里的原料是一份 73KB 的 TXT 文件——210 段餐厅描述,每段长这样:

The Gilded Artichokebrings abohemian chicenergy to the hills ofSilver Lake, operating as anupscale bistrothat prioritizesFarm-to-Table Californianingredients… Price range: $$$$

人读起来信息密度很高,但对程序来说这是灾难:

  • 没有统一 schema:菜系、评分、价格散落在句子的任何位置,正则写不完;
  • 类型不统一:评分是4.5/5,价格是$$$$,招牌菜是一句话里的短语;
  • 无法索引:想查"洛杉矶 300 元以内、有素食选项的融合菜",关键词匹配基本靠缘分。

这正是 LLM 结构化抽取的典型战场:用自然语言理解替代正则规则,用模式验证保证输出可控。整个管道的架构如下:

split('

')

通过

失败

超过重试上限

TXT 原文
210 段餐厅描述

段落列表
每段一家餐厅

LLM 抽取
One-shot 提示模板

输出清洗
剥离 ```json 标记

Pydantic
Schema 验证

结构化 JSON
入库

修复 LLM
错误信息回喂

跳过并告警

注意最后那条回边——验证失败不是终点,而是修复循环的起点。这是全文最重要的一处设计,后面细讲。

二、数据切分

文本按空行分段,加载和切分只需要三行:

file_path="California-Culinary-Map.txt"withopen(file_path,'r')asfile:data=file.read()restaurant_list=data.split('\n\n')# 按空行切段restaurant_list=restaurant_list[1:]# 第一段是标题,去掉

len(restaurant_list)得到 210。这里有个不起眼但值得强调的点:切段就是建立处理单元的边界契约。后续所有环节(抽取、验证、修复、入库)都以"段"为原子单位,任何一段失败都不应污染其他段。边界划得干净,后面的容错设计才有立足点——如果一段里混进两家餐厅,LLM 抽取和验证都会陷入混沌。

三、封装 LLM:面向 OpenAI 兼容协议编程

模型调用层用 LangChain 的ChatOpenAI封装,好处是换模型只改三个参数:

fromlangchain_openaiimportChatOpenAIfromlangchain_core.messagesimportSystemMessage,HumanMessagedefllm_model(system_msg,prompt_txt):model=ChatOpenAI(model="mimo-v2.6-flash",base_url="https://token-plan-cn.xiaomimimo.com/v1",api_key="...",)messages=[SystemMessage(content=system_msg),HumanMessage(content=prompt_txt)]response=model.invoke(input=messages)returnresponse.content

base_url+model+api_key三件套一换,实验里从 CSDN 的deepseek-v4-flash自由切换到小米的mimo-v2.6-flash。
在 OpenAI 兼容协议已经成为事实标准的今天,业务代码不应该绑定任何一家模型供应商——这对生产环境尤其重要。

另外套了一层带重试的包装,网络抖动时不至于让 210 次循环中途夭折:

defsafe_llm_call(system_msg,prompt_txt,retries=3):foriinrange(retries):try:returnllm_model(system_msg,prompt_txt)exceptException:time.sleep(2)return"Failed after retries"

四、One-shot 提示:把 schema 塞进示例里

1. 先明确要抽什么

从每段描述中提取 9 个字段:

字段含义原文形态目标类型
name餐厅名**The Gilded Artichoke**str
location位置Silver Lakestr
type餐厅类型upscale bistrostr
food_style餐饮风格Farm-to-Table Californianstr
rating评分4.5/5float
price_range价格区间$$$$int(美元符号个数)
signatures招牌菜句中短语List[str]
vibe氛围bohemian chicstr
environment环境描述一句话str
shortcomings不足可能缺失List[str]

其中price_range: $$$$ → 4是典型的隐式类型转换需求——人一眼看懂,但如果不明确指示,LLM 十有八九原样输出"$$$$"字符串。

2. One-shot 模板的设计

不用空口描述格式,直接给一对完整的输入-输出示例:

EXAMPLE_OUTPUT=""" { "name": "Mar de Cortez", "location": "Santa Monica", "type": "casual taqueria", "food_style": "Baja-style seafood", "rating": 4.2, "price_range": 1, "signatures": ["beer-battered snapper tacos", "zesty octopus ceviche"], "vibe": "salt-air energy", "environment": "a premier sun-drenched spot for open-air dining near the pier.", "shortcomings": [] } """defrestaurant_data_structure_prompt_generation(restaurant_paragraph):base_system_msg="你是一个专业的餐厅数据结构化处理助手。"base_user_prompt=f""" Task: 将餐厅描述结构化处理, 输出JSON格式。 Restaurant description:{restaurant_paragraph}Example: Input Restaurant Description:{EXAMPLE_RESTAURANT_PARAGRAPH}Output:{EXAMPLE_OUTPUT}"""returnbase_system_msg,base_user_prompt

实测单条抽取的输出:

{"name":"The Gilded Artichoke","location":"Silver Lake","type":"upscale bistro","food_style":"Farm-to-Table Californian","rating":4.5,"price_range":4,"signatures":["lavender-rubbed roasted chicken","delicate heirloom tomato tarts"],"vibe":"bohemian chic","environment":"a high-end greenhouse feel with reclaimed wood and floor-to-ceiling windows.","shortcomings":[]}

字段一个不少,$$$$正确转成了4,招牌菜准确抓出了两个。One-shot 的本质是把"格式规范"从抽象的文字描述变成具体的对齐目标——模型对示例的模仿能力远强于对指令的遵循能力,这也是 few-shot 提示在结构化输出场景几乎必用的原因。

五、Pydantic 验证:schema 即契约

LLM 输出"看起来像 JSON"不等于"是合法且符合业务的 JSON"。用 Pydantic 把数据契约正式化:

frompydanticimportBaseModel,Field,ValidationErrorfromtypingimportList,OptionalclassRestaurant(BaseModel):name:strlocation:strtype:strfood_style:strrating:Optional[float]=Noneprice_range:Optional[int]=Nonesignatures:List[str]=Field(default_factory=list)vibe:Optional[str]=Noneenvironment:strshortcomings:List[str]=Field(default_factory=list)restaurant_data=Restaurant.model_validate_json(json_data=test_response)

这个 schema 有两层设计考量:

  • 核心字段非空(name/location/type/environment是 str 必填):缺了就没法做检索,宁缺毋滥;
  • 易缺失字段 Optional + 默认值(rating、shortcomings等):原文里可能压根没提,硬性要求反而制造大量验证失败。

model_validate_json一句话同时完成JSON 语法校验 + 字段类型校验 + 缺失字段校验。更重要的是,它抛出的ValidationError自带结构化的错误详情(哪个字段、什么原因)——这份错误信息,正是下一环节自动修复的燃料。

六、自动修复:让 LLM 当自己的"格式审查员"

1. 为什么需要修复环节

批处理 210 条时,单条 95% 的成功率意味着期望失败 10 条左右。失败原因五花八门:字段名拼错、价格输出了字符串、signatures里混进了整句话。靠人工盯着改,210 条能把人改疯。

实验给出的方案是引入第二个 LLM 角色——修复专家,输入是"错误输出 + 验证错误详情",输出是修好的 JSON:

defJSON_auto_repair_prompts(candidate_json_output,error_message):auto_repair_system_msg=f""" 这是原始的错误输出:{candidate_json_output}; 这是错误信息:{error_message}"""auto_repair_prompt=f""" 根据错误信息{error_message}和原始非结构化脚本,修复{candidate_json_output}文件。 """returnauto_repair_system_msg,auto_repair_prompt

注意error_message用的是 Pydantic 的str(e.errors())——把结构化的错误详情(字段路径 + 违反原因)喂给修复模型,而不是只说"格式错了",修复的命中率完全不在一个量级。

2. 生成-验证-修复闭环

✓ 通过

✗ ValidationError

超出 max_attempts

LLM 抽取
生成候选 JSON

清洗 markdown 标记

Pydantic 验证

restaurant_data
加入结果集

提取 e.errors()
构造修复提示

修复 LLM

⚠️ 打印告警
跳过该条

主循环把三个环节串起来:

structured_restaurant_lists=[]fori,restaurant_paragraphinenumerate(restaurant_list):# 1. 生成候选 JSONbase_system_msg,base_user_prompt=restaurant_data_structure_prompt_generation(restaurant_paragraph=restaurant_paragraph)raw_response=llm_model(system_msg=base_system_msg,prompt_txt=base_user_prompt)candidate_json_output=raw_response.replace("```json","").replace("```","").strip()# 2. 验证 + 自动修复(限次)max_attempts=1# 防止无限循环current_attempt=0restaurant_data=Nonewhilecurrent_attempt<max_attempts:try:restaurant_data=Restaurant.model_validate_json(candidate_json_output)breakexceptValidationErrorase:current_attempt+=1auto_repair_system_msg,auto_repair_prompt=JSON_auto_repair_prompts(candidate_json_output,error_message=f"{str(e.errors())}")candidate_json_output=llm_model(system_msg=auto_repair_system_msg,prompt_txt=auto_repair_prompt)# 3. 成功才入库,失败告警跳过ifrestaurant_dataisnotNone:structured_restaurant_lists.append(restaurant_data)else:print(f"⚠️ 第{i+1}/{len(restaurant_list)}条餐厅数据修复失败,已跳过")if(i+1)%20==0:print(f'{i+1}out of{len(restaurant_list)}is done')print('ALL DONE!!')

跑完的实际结果:210 条输入,209 条入库,第 1 条修复失败被跳过。

3. 这个设计里藏着三个工程决策

① 为什么要限次(max_attempts=1)?修复 LLM 也是 LLM,不是确定性程序——修复失败后再喂回去重试,可能陷入"生成错 → 修复 → 还是错"的死循环,而每次循环都是真金白银的 token。限次 + 跳过,用完成率换确定性。

② 为什么跳过而不是中断?批处理的第一原则是单点失败不拖垮全局。209 条质量合格的数据,价值远大于 210 条里夹着一条手工兜底的脏数据。跳过的记录留下了告警日志,可以事后人工补。

③ "让 LLM 修 LLM"是不是最佳实践?说实话不是。生产级方案有更严格的约束手段:JSON mode / function calling 强制输出格式、解码层约束(如 outlines、guidance)、独立的校验重试框架。但在本场景中,抽取和修复对模型来说都是简单任务,成功率极高,引入重型框架的收益抵不上复杂度成本。工程选型的标准从来不是"理论最优",而是"够用且最简"。

七、入库:两个容易被忽略的收尾细节

structured_restaurant_lists_json=[r.__dict__forrinstructured_restaurant_lists]fori,responseinenumerate(structured_restaurant_lists_json):response['itemId']=1000001+i filename='structured_restaurant_data.json'withopen(filename,'w',encoding='utf-8')asf:json.dump(structured_restaurant_lists_json,f,indent=4)
  • __dict__转换:Pydantic 对象不能直接json.dump,先转成字典;
  • itemId对齐:分配1000001 + i的连续 ID,与后续用户评论数据集里的itemId对齐——这一步是在为多表关联预埋主键。结构化的最终目的不是存文件,而是让餐厅知识库和用户行为数据能够 JOIN 起来做推荐。schema 设计要往前看一步。

八、结语:这套管道的通用性

回头看,这个实验给出的不是"餐厅数据处理方案",而是一个可迁移的 LLM 结构化抽取模式:

任意非结构化文本
合同 / 简历 / 工单 / 日志

定义目标 Schema
Pydantic 模型

One-shot 模板
一对精选示例

批量生成 + 清洗

模式验证

错误回喂修复
限次 + 跳过

带主键的知识库
可索引 / 可关联

变的是数据和 schema,不变的是四条原则:

  1. 示例优于指令——想稳定拿到结构,就给它看一个结构;
  2. 验证先于信任——LLM 的输出在通过 schema 校验前,一律视为候选;
  3. 错误是资源——结构化的验证错误详情,是修复环节最精准的提示词;
  4. 提示词非常关键——好的提示词会大大提高执行效率,同时减少Token消耗;
  5. 批处理要有退路——限次、跳过、告警,用局部损失换全局确定。

这套管道的下一步,是把图片和 URL 也纳入知识库——用多模态模型给菜谱图片生成描述,让"视觉信息"变成可检索的文本。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 4:40:56

IE8-11全兼容URL安全Base64编解码方案

1. 项目概述&#xff1a;为什么一个“URL地址64位加密解密”功能&#xff0c;值得为IE全版本专门设计&#xff1f;在某次接手某高校教务系统前端重构任务时&#xff0c;我遇到一个看似简单、实则踩坑极深的需求&#xff1a;对跳转链接中的参数做轻量级混淆处理&#xff0c;防止…

作者头像 李华
网站建设 2026/10/15 4:39:38

JavaEE项目全局异常处理实战:从Filter到ExceptionMapper的演进

我发现很多同学做JavaEE项目时&#xff0c;异常处理还停留在“每个Service方法都try-catch&#xff0c;每个Controller方法都自己处理异常”的阶段。代码里充斥着重复的错误处理逻辑&#xff0c;一旦业务规则调整&#xff0c;所有方法的异常响应格式都要跟着改一遍&#xff0c;…

作者头像 李华
网站建设 2026/10/15 4:38:59

计算机组成与系统结构课后答案精讲:MIPS/CPI、IEEE754与校验码全解析

简介&#xff1a;计算机组成与系统结构&#xff08;袁春风第三版&#xff09;课后习题答案汇总&#xff0c;面向计算机专业本科生、考研备考生及自学人员&#xff0c;系统覆盖指令系统、数据表示、性能评估、时钟频率与CPI等核心知识点。资源包约1.75MB&#xff0c;内含1个doc格…

作者头像 李华
网站建设 2026/10/15 4:38:59

Flutter×OpenHarmony跨端实战:维修服务项目模块开发全解析

去年有一段时间&#xff0c;我一直被一个问题反复困扰&#xff1a;门店里的维修工单、报价单、配件库存全靠纸质本子和Excel来回倒腾&#xff0c;客户等着报价&#xff0c;前台却要翻半天历史记录才能确定一个常用项目的工时价格。甲方找到我&#xff0c;提了一个很实际的需求—…

作者头像 李华
网站建设 2026/10/15 4:38:49

动态线程池实战:从ThreadPoolExecutor参数调优到线上救火

前阵子看到某大厂分享的“动态线程池”实践&#xff0c;第一反应有点不以为然&#xff1a;线程池还能玩出花&#xff1f;参数不就是corePoolSize、maximumPoolSize、队列长度这些吗&#xff0c;把值改一改谁不会&#xff1f;直到后来自己负责的服务因为固定参数线程池在大促前夜…

作者头像 李华
网站建设 2026/10/15 4:38:44

做钣金10年,我们只干一件事:搞定高难度小型精密异形件

做钣金10年&#xff0c;我们只干一件事&#xff1a;搞定高难度小型精密异形件在钣金行业&#xff0c;有一个很现实的现象&#xff1a;简单大件&#xff0c;无数工厂抢着做复杂小件、精密异形件、难折弯、难成型的工件&#xff0c;没人愿意做。但高新设备、医疗、AI、无人机、汽…

作者头像 李华