
简介基于LLM的命名实体识别与关系抽取项目包面向自然语言处理方向的学生、研究人员及企业开发者解决利用ChatGLM、GPT、LLaMA等大模型完成NER与实体关系抽取任务时的代码实现与工程组织问题。资源共13个文件以12个Python脚本为主另附1个README说明文档整体仅50KB脚本覆盖chatglm4ner、gpt4ner、llama4ner等多个系列对应不同模型的调用、示例与推理逻辑项目目录中data、uer、models三个模块分别管理数据、预训练大模型与模型保存结构清晰便于按需替换与二次开发。已有124人浏览/学习下载代码均经过测试运行成功可放心使用适合作为课程设计、毕业设计或项目初期演示的参考也支持在此基础上扩展其他信息抽取功能。遇到运行疑问可联系作者咨询便于初学者快速上手。1. LLM 让 NER/IE 从标注工程变成协议工程早几年做 NER团队里最消耗的不是模型结构而是标注规范。实体类型、边界判定、嵌套实体、指代消解每一条都要写进标注文档关系抽取更麻烦一个“购买”关系在文本里可以横跨三句话。LLM 出现后很多人以为 NER/IE 会被一个 prompt 解决真上了生产才发现瓶颈从“标多少数据”变成了“把实体清单和关系定义说清楚”。这篇内容聊的是基于 LLM 的 NER/IE 落地路线怎么设计抽取协议、怎么用少样本 prompt 快速验证、什么情况下必须微调、评测该盯哪些指标、长文档怎么处理。适合两类人一类是刚接手知识抽取项目的新手另一类是被标注成本压得想换方案的工程师。“零标注”不是零样本后面的例子会说明白。2. LLM 做 NER/IE 的三种范式与选型依据在动 prompt 之前值得先花三分钟把传统 NER/IE 的底子翻出来。很多人直接从 LLM 入手遇到边界错误、关系漏抽时反而不知道问题出在哪。LLM 之前的最优解是序列标注把每个 token 标记成 BIOES 标签B 代表实体开头I 代表实体内部E 代表结尾S 代表单字实体O 代表非实体。模型预测每个 token 的标签再用 CRF 层把标签序列串起来。你问它“三菱FX5U PLC”是设备“CC-Link IE Basic”是协议它输出的是一个标签路径。2.1 序列标注范式CRF 路径约束为什么仍然重要CRF 的关键在转移矩阵B-PER 后面可以接 I-PER 或 E-PER不能接 I-LOC。这种路径约束正是 LLM 生成式解码所没有的。你可以让模型输出 JSON但没法在 token 概率层面禁止“I-LOC 出现在 B-PER 之后”这种序列错误。这也是 LLM 做抽取时实体边界漂移的根源模型端生成概率分布偏向常见词推理端又没有标签转移约束只能靠输出层限制。工程上常见的补法有两条。一条是给模型提供候选实体列表让它从列表里挑而不是凭空生成另一条是在解码阶段用约束解码把输出限制在合法的 JSON schema 内。理解这一点你才能解释为什么同一个模型在两次调用里同一段文本会抽出不同的实体边界。BIOES 时代不会出现这种问题因为 CRF 的转移矩阵从训练数据里学死了。2.2 生成式抽取的三种路线LLM 做抽取有三条常见路线各有取舍我分头说。第一条是 span 抽取模型直接返回原文子串的起止位置。优点是实体文本绝对忠实原文适合专有名词、型号、报错代码不能被改写的场景。缺点是上下文窗口一旦超过几百个 token位置偏移错误率快速上升模型经常把 start/end 算错一位。第二条是结构化 JSON 生成模型返回嵌套的 entities/relations 列表。这是目前最主流的做法开发效率最高schema 变更只改 prompt 不改代码。缺点是需要做 schema 校验和失败的兜底重试。第三条是自由文本加后处理解析让模型先用自然语言描述抽取结果再用规则或小模型转成结构化数据。我一般不推荐因为后处理解析又绕回了传统算法模型输出格式稍微变一下规则就要跟着改错误是叠加的。范式优点缺点适用场景span 抽取实体文本不改写、可对齐原文长文位置偏移高短文本、需要精确回填JSON 生成结构化程度高、schema 灵活需要约束解码加校验开放域、schema 频繁变更文本加后处理prompt 约束最宽松错误叠加、维护成本高临时演示、不推荐生产2.3 判别式与生成式的选型依据选型标准其实不复杂。实体类型超过 20 种、关系超过 10 种且 schema 每个月都在变选生成式方案因为你不可能每加一种实体就重新标注训练一轮。反过来实体集合封闭、类型固定团队又有标注人力判别式加规则插槽仍然可以做到实体级 F1 到 98%没必要引入 LLM 的不确定性。还有一类介于两者之间已知实体清单是产品型号、地名、人名这种封闭集合。模型端背不住全部候选值推理端也不可能穷举。我一般会先把已知库做向量化按文本相似度召回 Top 50 拼进 System Prompt让模型从候选里挑。这就是轻量级 RAG 增强 LLM 的做法适合实体种类固定但实例频繁新增的场景。它不需要微调也不能微调因为实例每天都在更新。3. 用兼容 LLM API 做零标注 NER/IE 的最小闭环所谓零标注指不需要准备训练数据但必须准备几个示例样本。你可以把三步压缩成一件事定义 schema把 schema 和样例写进 prompt调用模型并解析输出。大多数云厂商的模型接口都兼容 OpenAI 的 chat completion 协议所以只要学会这一种格式迁移到本地部署的 LLM 框架时只需要改 base_url 和 api_key。3.1 先定义 schema 再写 prompt先定义信息抽取 schema包括实体类型、关系类型、输出格式。schema 定义得越清楚后面出错的概率越低。一个容易忽略的细节关系类型必须同时声明 subject 和 object 的实体类型否则模型会造出“控制(PLC, 参数)”这种跨类型的错误三元组。{ entities: [设备, 协议, 指令, 参数], relations: [ {type: 控制, subject_type: 设备, object_type: 设备}, {type: 通过, subject_type: 设备, object_type: 协议}, {type: 设置, subject_type: 设备, object_type: 参数} ], output_format: { entities: [{text: string, type: string}], relations: [{subject: string, predicate: string, object: string}] } }这段 JSON 直接拼进 System Prompt模型就能按照约束输出。如果你用的是 Dify 这类低代码编排平台把这段 JSON 写进提示词变量再在模型设置页把 Temperature 调成 0效果是一样的。关键原则是输出的 JSON 结构要和下游的 IE 消费端兼容实体字段名不要用 alias否则清洗逻辑要多写一层映射。schema 做好后先挑 3 到 5 条典型样本测试不要一上来就批量跑。3.2 一个可直接运行的 Python 抽取脚本下面这个脚本是我日常验证 prompt 的最小模板替换 base_url 和 api_key 就能直接用。import json from openai import OpenAI client OpenAI( base_urlhttp://localhost:8000/v1, api_keyEMPTY ) entity_types [设备, 协议, 指令, 参数] relation_types [控制, 通过, 设置] def extract(text, examples): system_prompt ( 你是信息抽取引擎。只输出 JSON不要输出任何解释。\n f实体类型: {, .join(entity_types)}\n f关系类型: {, .join(relation_types)}\n 输出格式: {entities: [{text: , type: }], relations: [{subject: , predicate: , object: }]}\n 要求实体文本必须来自原文不得改写 关系三元组的主语和宾语必须已经在实体列表中出现。 ) messages [{role: system, content: system_prompt}] for ex in examples: messages.append({role: user, content: ex[text]}) messages.append({role: assistant, content: json.dumps(ex[result], ensure_asciiFalse)}) messages.append({role: user, content: text}) resp client.chat.completions.create( modelyour_model_name, messagesmessages, temperature0, seed42, response_format{type: json_object}, max_tokens4096 ) content resp.choices[0].message.content try: return json.loads(content) except json.JSONDecodeError: # 截断时常见异常尝试提取第一个 { 到最后一个 } start content.find({) end content.rfind(}) return json.loads(content[start:end1])代码里几个参数要说明。temperature0 不等于完全确定性但能把随机性压到最低seed42 只在你固定使用同一版本模型时有意义换模型后不要迷信同 seed 可复现。response_format 触发 JSON 模式很多本地推理框架不一定支持不支持时兜底做法是去掉这个参数在 System Prompt 里强调“只输出 JSON”。max_tokens 建议给大一点抽取长文本时 JSON 被截断是最高频错误宁可多花一点 token 也不要在解析阶段失败。examples 是少样本示例每个示例需要同时提供原文和期望结果能显著降低格式错误率。3.3 输出合法性的三个参数参数调优往往比换模型更有效。我总结了一张参数表适合绝大多数抽取任务。参数推荐值作用temperature0减少随机采样导致的实体边界漂移seed固定整数保障调试阶段可复现response_formatjson_object强制模型走 JSON 输出分支max_tokens4096防止长文本输出被截断top_p1关闭动态采样配合 temperature 使用一个常见误区top_p 和 temperature 同时调低并不会叠加收益这两个参数的采样路径不同工程上只需把 temperature 置 0、top_p 置 1。如果模型输出频繁越界优先检查是不是实体类型定义太抽象例如“设备”和“组件”在中文语料里边界模糊模型不知道该归哪一类这类问题调参解决不了只能改 schema。另一种做法是对同一个文本调用三次用投票的方式取多数结果能进一步提升实体边界准确率代价是 API 成本和延迟翻三倍。3.4 先实体后关系的两段式抽取一次调用同时输出实体和关系模型注意力会被分散关系三元组经常出现主语不在实体列表里的情况。我一般会拆成两段第一段只抽实体第二段把实体列表拼进 prompt再做关系抽取。这样模型不需要边找实体边抽关系错误率明显下降。def extract_two_pass(text, examples): # pass 1: 只抽实体 entities extract(text, examples)[entities] entity_desc .join(e[text] for e in entities) # pass 2: 把实体列表注入约束关系抽取 relation_prompt ( f已知实体列表{entity_desc}\n 只允许使用上面的实体作为关系三元组的主语和宾语 不要发明新实体不要改写实体文本。 ) return extract(relation_prompt \n text, examples)这里的两段式本质上就是一个最小的 LLM Agent 流水线第一段是工具调用第二段把工具结果作为上下文约束。如果项目引入了 LangChain可以用 tool selector 把第二段封装成关系抽取工具按文本类型动态决定是否调用。两段式代价是延迟约翻倍适合对准确率要求高的场景如果只是粗筛数据单次调用加一个后置校验就够了。4. 领域微调数据准备、损失函数与训练参数通用 prompt 做 NER/IE最好的情况能到 85% 的实体 F1再往上就比较吃力。领域术语缩写、实体边界和语料分布强绑定的场景例如“CC-Link IE Basic”被拆成两个实体就得考虑微调。微调不是起点prompt 调优和示例增强解决不了再走这条路从学习路线上看SFT 排在 in-context learning 和 RAG 之后不是第一步。4.1 什么情况下才需要微调我给一个简单的判断标准先拿 100 条人工标注样本做 few-shot 验证如果实体边界错误率超过三成或者某个关系类型漏检严重才需要微调。还有一种典型情况是语料风格和通用预训练语料差异太大比如设备手册、病历、法律文书模型对领域缩写毫无概念几个示例根本教不会。微调之前先把数据配比想清楚。实体抽取任务的数据不是越多越好而是越干净越好。1000 条高质量样本往往好过 10000 条噪声样本因为损失函数会对重复出现的错误模式反复加权一条标注错的实体边界会造成模型对某个词形成错误偏好。数据准备阶段一定要检查三件事实体文本是否完整覆盖原文、实体类型是否互斥、关系三元组的主语宾语是否存在于实体列表。4.2 把标注数据转换成 ChatML 指令SFT 数据格式建议统一成 ChatML 结构system 固定指令、user 放文本、assistant 放 JSON 结果。传统标注工具导出的是 BIOES 标签或 BRAT 格式需要转换成这个结构。import json def to_chatml(record): text record[text] entities [ {text: e[start] and text[e[start]:e[end]], type: e[type]} for e in record[entities] ] relations [ {subject: r[subject], predicate: r[type], object: r[object]} for r in record[relations] ] assistant json.dumps( {entities: entities, relations: relations}, ensure_asciiFalse ) return { messages: [ {role: system, content: 你是信息抽取引擎只输出 JSON。}, {role: user, content: text}, {role: assistant, content: assistant} ] }这段代码把标注结果序列化成 assistant 输出正好对应训练时的目标文本。注意e[start] and text[...]是为了防止 start 字段缺失直接抛异常实际工程里建议在数据清洗阶段就把空实体过滤掉。转换完成后我习惯用 LLM Studio 这类可视化微调工具先跑一次小规模训练观察 loss 曲线是否正常下降再进入正式训练。如果 loss 不降多半是数据里混入了空 JSON 或者格式不统一。4.3 SFT 训练参数与损失计算范围参数设置上我给出一个经过多次项目验证的基准组合具体数值随语料长度微调。参数推荐值说明learning_rate1e-5 到 2e-5高于 5e-5 容易灾难性遗忘epochs2 到 3超过 5 轮过拟合明显batch_size16显存不够就才用梯度累积max_length2048实体跨长句时需要加长LoRA rank8 到 16领域差异大时用 16loss mask只计算 response不计算 user 和 system 部分损失函数这里有一个普遍踩坑点很多开源 SFT 脚本默认对整条 sequence 计算交叉熵导致模型在预测 user 指令文本上也花了优化空间。这个问题的来源在预训练损失函数设计里就有体现传统语言模型对所有 token 平等计算损失但指令微调要的是只对 assistant 部分计算。训练时的模板设置里必须开启loss-on-response-only否则模型会变得能模仿指令格式但抽取质量上不去。4.4 垂直场景样例设备手册里的实体关系抽取拿一条真实场景举例。设备说明书原句“三菱FX5U PLC通过CC-Link IE Basic控制伺服。”这里的实体有“三菱FX5U PLC”和“伺服”关系是控制还有一条通过关系指向协议“CC-Link IE Basic”。注意这个 IE 是工业以太网协议名不是信息抽取的 IE训练数据里这类歧义词必须保留在原文语境中模型才能学会用上下文区分。数据准备时要同时构造负样本例如“三菱FX5U PLC安装在电控柜中”这句话只有实体、没有关系模型需要学会输出空 relations 数组而不是硬凑一个“安装”关系。负样本比例控制在 30% 以上能显著降低幻觉。垂域 LLM 数据准备的核心就在这一步实体边界、关系方向、负样本三者平衡而不是只堆正样本。数据量不足 500 条时优先用标注一致性审查替代加数据两个人对同一批文本分别标注用 Cohen‘s kappa 找分歧样本分歧往往是 schema 定义不清。5. 长文档抽取的跨段窗口与一致性自检真实生产环境里文本经常超出上下文窗口。常见做法是分段但分段本身会切断跨句关系。一个设计良好的抽取管线要同时处理窗口切分、实体去重和输出校验三个问题。这一节我给出一个两段式组合滑窗分段加一致性自检。5.1 滑窗分段与重叠区去重def chunk_text(text, chunk_size1500, overlap200): chunks [] start 0 while start len(text): end min(start chunk_size, len(text)) chunks.append(text[start:end]) if end len(text): break start max(start chunk_size - overlap, 0) if start end: break return chunks分段参数说明chunk_size 按中文字符数算1500 字大约是 2000 到 2500 tokenoverlap 取 200 到 300 字用来兜住被切断的长实体。实体文本长度超过 overlap 时仍然会被切碎这种情况应该在分片前先做一轮长度检查把超长段落单独处理。重叠区里同一个实体会在相邻两个 chunk 各出现一次合并结果时以实体文本加类型作为 key 去重关系不用去重因为跨 chunk 的同一关系大概率分别在两个 chunk 里各抽一次。5.2 用一致性自检替代随机抽检抽检只能发现问题不能防止问题扩散。更可靠的做法是写一个一致性校验函数对每条抽取结果做规则检查把错误结果筛出来重新调用模型修复。def validate(result, schema): errors [] entity_texts {e[text] for e in result[entities]} for rel in result[relations]: if rel[predicate] not in schema[relations]: errors.append(f非法关系类型: {rel[predicate]}) if rel[subject] not in entity_texts: errors.append(f主语不在实体列表: {rel[subject]}) if rel[object] not in entity_texts: errors.append(f宾语不在实体列表: {rel[object]}) for e in result[entities]: if e[type] not in schema[entities]: errors.append(f非法实体类型: {e[type]}) return errors这个校验函数看起来简单在项目里价值很高。它检查了三个最常出问题的点关系类型是否越界、三元组是否引用未识别实体、实体类型是否属于 schema。校验失败的结果不要直接丢弃把错误信息拼回去再调用一次模型让模型基于错误信息修复输出我在真实项目里这种重试可以把格式合规率从 92% 拉到 99% 以上。最后一件事是把这段校验函数接进回归测试每次换模型版本、改 prompt、升级推理框架之前用同一份 benchmark 语料跑一遍对比实体重叠度和关系准确率指标变化。这样模型迭代不再依赖人工抽检回归自动挡在发布前。本文还有配套的精品资源点击获取