首页 / 资讯中心 / 文章详情

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

[实践]-用LLM对文本结构化:把多段文本描述变成规范的 JSON 知识库 ★ FEATURED ARTICLE
本次实践以一个真实的推荐系统数据工程场景为背景把 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 标记PydanticSchema 验证结构化 JSON入库修复 LLM错误信息回喂跳过并告警注意最后那条回边——验证失败不是终点而是修复循环的起点。这是全文最重要的一处设计后面细讲。二、数据切分文本按空行分段加载和切分只需要三行file_pathCalifornia-Culinary-Map.txtwithopen(file_path,r)asfile:datafile.read()restaurant_listdata.split(\n\n)# 按空行切段restaurant_listrestaurant_list[1:]# 第一段是标题去掉len(restaurant_list)得到 210。这里有个不起眼但值得强调的点切段就是建立处理单元的边界契约。后续所有环节抽取、验证、修复、入库都以段为原子单位任何一段失败都不应污染其他段。边界划得干净后面的容错设计才有立足点——如果一段里混进两家餐厅LLM 抽取和验证都会陷入混沌。三、封装 LLM面向 OpenAI 兼容协议编程模型调用层用 LangChain 的ChatOpenAI封装好处是换模型只改三个参数fromlangchain_openaiimportChatOpenAIfromlangchain_core.messagesimportSystemMessage,HumanMessagedefllm_model(system_msg,prompt_txt):modelChatOpenAI(modelmimo-v2.6-flash,base_urlhttps://token-plan-cn.xiaomimimo.com/v1,api_key...,)messages[SystemMessage(contentsystem_msg),HumanMessage(contentprompt_txt)]responsemodel.invoke(inputmessages)returnresponse.contentbase_urlmodelapi_key三件套一换实验里从 CSDN 的deepseek-v4-flash自由切换到小米的mimo-v2.6-flash。在 OpenAI 兼容协议已经成为事实标准的今天业务代码不应该绑定任何一家模型供应商——这对生产环境尤其重要。另外套了一层带重试的包装网络抖动时不至于让 210 次循环中途夭折defsafe_llm_call(system_msg,prompt_txt,retries3):foriinrange(retries):try:returnllm_model(system_msg,prompt_txt)exceptException:time.sleep(2)returnFailed after retries四、One-shot 提示把 schema 塞进示例里1. 先明确要抽什么从每段描述中提取 9 个字段字段含义原文形态目标类型name餐厅名**The Gilded Artichoke**strlocation位置Silver Lakestrtype餐厅类型upscale bistrostrfood_style餐饮风格Farm-to-Table Californianstrrating评分4.5/5floatprice_range价格区间$$$$int美元符号个数signatures招牌菜句中短语List[str]vibe氛围bohemian chicstrenvironment环境描述一句话strshortcomings不足可能缺失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_promptf 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_factorylist)vibe:Optional[str]Noneenvironment:strshortcomings:List[str]Field(default_factorylist)restaurant_dataRestaurant.model_validate_json(json_datatest_response)这个 schema 有两层设计考量核心字段非空name/location/type/environment是 str 必填缺了就没法做检索宁缺毋滥易缺失字段 Optional 默认值rating、shortcomings等原文里可能压根没提硬性要求反而制造大量验证失败。model_validate_json一句话同时完成JSON 语法校验 字段类型校验 缺失字段校验。更重要的是它抛出的ValidationError自带结构化的错误详情哪个字段、什么原因——这份错误信息正是下一环节自动修复的燃料。六、自动修复让 LLM 当自己的格式审查员1. 为什么需要修复环节批处理 210 条时单条 95% 的成功率意味着期望失败 10 条左右。失败原因五花八门字段名拼错、价格输出了字符串、signatures里混进了整句话。靠人工盯着改210 条能把人改疯。实验给出的方案是引入第二个 LLM 角色——修复专家输入是错误输出 验证错误详情输出是修好的 JSONdefJSON_auto_repair_prompts(candidate_json_output,error_message):auto_repair_system_msgf 这是原始的错误输出{candidate_json_output}; 这是错误信息{error_message}auto_repair_promptf 根据错误信息{error_message}和原始非结构化脚本修复{candidate_json_output}文件。 returnauto_repair_system_msg,auto_repair_prompt注意error_message用的是 Pydantic 的str(e.errors())——把结构化的错误详情字段路径 违反原因喂给修复模型而不是只说格式错了修复的命中率完全不在一个量级。2. 生成-验证-修复闭环✓ 通过✗ ValidationError超出 max_attemptsLLM 抽取生成候选 JSON清洗 markdown 标记Pydantic 验证restaurant_data加入结果集提取 e.errors()构造修复提示修复 LLM⚠️ 打印告警跳过该条主循环把三个环节串起来structured_restaurant_lists[]fori,restaurant_paragraphinenumerate(restaurant_list):# 1. 生成候选 JSONbase_system_msg,base_user_promptrestaurant_data_structure_prompt_generation(restaurant_paragraphrestaurant_paragraph)raw_responsellm_model(system_msgbase_system_msg,prompt_txtbase_user_prompt)candidate_json_outputraw_response.replace(json,).replace(,).strip()# 2. 验证 自动修复限次max_attempts1# 防止无限循环current_attempt0restaurant_dataNonewhilecurrent_attemptmax_attempts:try:restaurant_dataRestaurant.model_validate_json(candidate_json_output)breakexceptValidationErrorase:current_attempt1auto_repair_system_msg,auto_repair_promptJSON_auto_repair_prompts(candidate_json_output,error_messagef{str(e.errors())})candidate_json_outputllm_model(system_msgauto_repair_system_msg,prompt_txtauto_repair_prompt)# 3. 成功才入库失败告警跳过ifrestaurant_dataisnotNone:structured_restaurant_lists.append(restaurant_data)else:print(f⚠️ 第{i1}/{len(restaurant_list)}条餐厅数据修复失败已跳过)if(i1)%200:print(f{i1}out of{len(restaurant_list)}is done)print(ALL DONE!!)跑完的实际结果210 条输入209 条入库第 1 条修复失败被跳过。3. 这个设计里藏着三个工程决策① 为什么要限次max_attempts1修复 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]1000001i filenamestructured_restaurant_data.jsonwithopen(filename,w,encodingutf-8)asf:json.dump(structured_restaurant_lists_json,f,indent4)__dict__转换Pydantic 对象不能直接json.dump先转成字典itemId对齐分配1000001 i的连续 ID与后续用户评论数据集里的itemId对齐——这一步是在为多表关联预埋主键。结构化的最终目的不是存文件而是让餐厅知识库和用户行为数据能够 JOIN 起来做推荐。schema 设计要往前看一步。八、结语这套管道的通用性回头看这个实验给出的不是餐厅数据处理方案而是一个可迁移的 LLM 结构化抽取模式任意非结构化文本合同 / 简历 / 工单 / 日志定义目标 SchemaPydantic 模型One-shot 模板一对精选示例批量生成 清洗模式验证错误回喂修复限次 跳过带主键的知识库可索引 / 可关联变的是数据和 schema不变的是四条原则示例优于指令——想稳定拿到结构就给它看一个结构验证先于信任——LLM 的输出在通过 schema 校验前一律视为候选错误是资源——结构化的验证错误详情是修复环节最精准的提示词提示词非常关键——好的提示词会大大提高执行效率同时减少Token消耗批处理要有退路——限次、跳过、告警用局部损失换全局确定。这套管道的下一步是把图片和 URL 也纳入知识库——用多模态模型给菜谱图片生成描述让视觉信息变成可检索的文本。
阅读完成 · 觉得有帮助?
咨询建站