简介:面向农业园林从业者与AI技术应用人员,DeepSeek农业园林杂草绿色防除方案系统讲解如何基于实体抽取与语义检索实现杂草种类识别,并自动生成生态防除措施。整个文档共539页、51个大章节,从数据采集与标注、特征工程、模型训练与蒸馏,到向量化编码、语义检索索引构建与方案生成,完整覆盖一条可落地的技术链路。其中重点阐述了实体抽取规则设计、损失函数优化、模型微调与蒸馏、实体消歧、HNSW索引结构等关键模块,帮助读者理解大模型在实际农业场景中的工程化方法。资源为单个PDF文件,大小14.96MB,支持目录跳转与书签大纲,文字图表显示正常,便于按章节查阅和快速定位。目前已有58人学习浏览,适合希望借助大模型技术解决农业园林杂草识别与绿色防除问题的开发者、研究者及植保领域人员参考。
1. DeepSeek农业园林杂草绿色防除方案:先搞清楚这份539页PDF到底解决什么问题
给水稻田拍张照片,三十秒内告诉你田里那几株草是稗草还是千金子,再补一句“先别急着打除草剂,试试排水晒田”——这是DeepSeek农业园林杂草绿色防除方案想解决的事。这份539页的PDF不是一本给人通读的杂草图鉴,而是一条“机器先检索、人再做决定”的技术路线:实体抽取让杂草知识从大段文字变成结构化字段,语义检索把用户的提问和PDF里最相关的防除经验连起来,最后由大模型生成一份可执行的生态防除方案。
说白了,它解决的是农技员翻书慢、凭经验判断、除草剂滥用这三个老问题。适合两类人照着复现:一类是做农业信息化、植保知识库、农技问答系统的开发者,另一类是手头有大量植保PDF文档、想用DeepSeek把它们盘活的技术负责人。接下来我会按“原理 → 最小落地系统 → 方案生成 → 踩坑 → 验证”的顺序,把整条链路拆开讲。
2. 先从原理说起:实体抽取与语义检索为什么是杂草识别的“左膀右臂”
2.1 实体抽取在杂草识别里抽的是什么:从非结构化文本到结构化字段
539页PDF里绝大部分是描述性文字,比如“叶片条形,无柄,叶舌膜质”“多生于湿润农田,与水稻争夺养分”。这类文字人读得懂,但大模型没有逐页读过这份PDF,直接让它回答“这株草是什么、怎么防”等于让它凭空猜。实体抽取要做的事,就是把PDF里的知识切成一个个字段,让大模型在回答时能精确引用原文。
我一般会抽取六类实体:杂草名称(中文俗名和拉丁学名分开)、科属分类、形态特征、生长周期与发生规律、危害作物、防治方法。其中“杂草名称”是主键,后面所有检索和方案生成都以它为核心。实体抽取用DeepSeek做离线批处理,不放在线上请求里,这样线上只做向量检索和生成,成本和延迟都可控。
import json from openai import OpenAI client = OpenAI( api_key="sk-xxxx", # 从环境变量读取,不要写死在代码里 base_url="https://api.deepseek.com" ) SYSTEM_PROMPT = """ 你是植保领域的信息抽取器。请从文本中抽取以下实体: 1. weed_name:杂草名称(中文俗名列表,含别名) 2. latin_name:拉丁学名 3. family:科属分类 4. morphology:形态特征(高度、叶形、花序等) 5. lifecycle:生长周期与发生规律 6. host_crops:危害作物 7. control_methods:防治方法(物理、化学、生物、生态) 规则:weed_name必须是具体种名,禁止输出“禾本科杂草”这类上位概念。 只输出JSON对象,不要输出任何解释文字。 """ def extract_weed_entities(text_chunk: str) -> dict: resp = client.chat.completions.create( model="deepseek-chat", # 模型名以DeepSeek开放平台当前列表为准 messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": text_chunk} ], response_format={"type": "json_object"}, # 强制JSON输出 temperature=0.1, max_tokens=1500 ) return json.loads(resp.choices[0].message.content)这段代码的逻辑是:用system prompt把实体schema定死,让DeepSeek只做抽取不做扩展。temperature=0.1是因为抽取任务要稳定,不能让同一段文字两次抽出的字段不一样。response_format={"type": "json_object"}是让模型输出合法JSON,省去后面解析的麻烦。
实操里有个容易翻车的点:同一种草有多个俗名,比如“千金子”也叫“油草”“雀麻草”。如果实体抽取每次都原样输出,同一个草种在知识库里会有一堆重复条目。我一般会在抽取后加一层“实体归并”:
CANONICAL_ALIASES = { "油草": "千金子", "雀麻草": "千金子", "节节草": "木贼" # 注意同名异物的坑,后续细讲 } def canonicalize_weed_name(raw_name: str) -> str: return CANONICAL_ALIASES.get(raw_name, raw_name)这段逻辑不复杂,但很关键。别名表和草种对照关系建议让植保人员审核一遍,别全信模型,也别全信网上找的别名表——同一俗名在不同地区指不同植物的案例太多了。
2.2 语义检索解决什么:向量召回与混合检索
实体抽取把PDF变成了结构化条目,但用户的提问往往不是“请查一下千金子的防治方法”,而是“稻田里叶子带毛、分蘖很多的杂草是什么”。这种描述和PDF里的“叶片两面被微毛”在字面上完全不同,用关键词搜索根本搜不到。语义检索就是解决“词面不匹配、语义相近”的问题:把文本变成向量,用余弦相似度找最接近的段落。
我会把每一个chunk(切好的文本块)和用户query分别做embedding,然后算相似度取Top-K。最轻量的做法是直接存numpy矩阵,数据量在几万条以内时完全够用,不需要上FAISS。
import numpy as np def embed(text: str) -> list: resp = client.embeddings.create( model="deepseek-embedding", # 以DeepSeek开放平台实际提供的embedding模型为准 input=text ) return resp.data[0].embedding def semantic_search(query: str, chunk_records: list, top_k: int = 5) -> list: q_vec = np.array(embed(query)) scored = [] for rec in chunk_records: vec = np.array(rec["vector"]) sim = np.dot(q_vec, vec) / (np.linalg.norm(q_vec) * np.linalg.norm(vec)) scored.append({"chunk": rec["chunk"], "score": sim, "meta": rec["meta"]}) scored.sort(key=lambda x: x["score"], reverse=True) return scored[:top_k]这里的核心参数是top_k。我建议先用top_k=5跑一批真实问题,观察召回的段落里有没有正确答案。如果前5个里有3个以上是无关内容,说明chunk切得太碎或者embedding模型选得不对;如果前5个都相关但互相重复,就要引入去重逻辑,后面会细说。
纯向量检索有个盲区:除草剂名称、拉丁学名这类专有名词,语义相似度不一定高,但字面完全一致。常见做法是加一层BM25倒排索引做混合检索,向量召回和关键词召回合并后重新排序。这样做的好处是“氯氟吡氧乙酸”这种词能被精确命中,而“像韭菜一样的草”这种描述能靠向量兜住。技术社区里很多RAG项目都走这个路线,不算什么新方案,但确实管用。
2.3 为什么这套技术栈能装下539页PDF:内容规模与更新成本
539页看似吓人,但拆开看成本是可算的。按每页500字有效内容估,全本约25万字,embedding一次大概消耗几十万token,按DeepSeek的价格算下来是几块到几十块钱的量级,属于一次性成本。真正贵的不是embedding,而是后面反复调prompt时重新跑实体抽取的迭代成本。
我的习惯是先把PDF整体做一次实体抽取和向量化,存成结构化文件作为基线;后续修订PDF时,只重新处理变化的章节,而不是全书重跑。这样知识库的维护成本就控制住了。另一点值得注意:线上查询链路不要带实体抽取,只做两件事——把用户query向量化,然后从库里取Top-K段落给生成模型。这样单次请求的成本很低,也方便用缓存扛流量。
3. 把方案拆成最小落地系统:本地向量库与DeepSeek API/本地部署的搭建步骤
3.1 先选型:DeepSeek API还是本地部署
选型决定后面所有代码怎么写,我一般用这个标准衡量:数据是否敏感、调用频次多高、有没有空闲GPU。三个条件里占两个再考虑本地部署,否则直接用API。
| 方案 | 需要GPU | 延迟 | 隐私 | 适合场景 |
|---|---|---|---|---|
| DeepSeek API | 不需要 | 受网络影响 | 数据出网 | 原型验证、调用量低 |
| vLLM部署DeepSeek开源模型 | 24G以上显存 | 低 | 数据不出内网 | 高频调用、数据敏感 |
| Ollama本地部署量化模型 | 16G以上显存 | 低 | 数据不出内网 | 单机演示、离线开发 |
注意表格里“24G以上显存”是个经验值,具体取决于你部署的模型参数和量化方式。如果只是跑通流程,API最快;如果是给生产环境用,我建议一步到位上vLLM,因为它的并发控制和显存管理比Ollama成熟。技术社区里用“vllm部署deepseek”的讨论很多,照着官方示例配一个服务就好,不一定非要用原项目的部署脚本。
3.2 第一步:把539页PDF切分成可检索的chunk并建立向量库
这步是重头戏,chunk切得好不好直接影响检索质量。我有一次图省事按每1000字硬切,结果把“猪殃殃的形态特征”和“猪殃殃的化学防除”切成两半,查询“猪殃殃怎么除”时召回了形态特征段落,方案生成只能靠模型硬编,效果很离谱。后来改成“保留段落结构 + 固定长度兜底”的切法,问题就解决了。
import fitz # PyMuPDF import re def extract_pdf_text(pdf_path: str) -> str: doc = fitz.open(pdf_path) pages = [] for page in doc: text = page.get_text("text") pages.append(text) return "\n".join(pages) def chunk_text(text: str, chunk_size: int = 500, overlap: int = 50) -> list[str]: # 先按空行切段落,保留段落完整性 paragraphs = re.split(r"\n\s*\n", text) chunks = [] buf = "" for para in paragraphs: if len(buf) + len(para) > chunk_size and buf: chunks.append(buf.strip()) # overlap保留上一块末尾内容,避免切点截断关键信息 buf = buf[-overlap:] + "\n" + para else: buf += "\n" + para if buf.strip(): chunks.append(buf.strip()) return chunks这段代码的逻辑是:先按空行把PDF拆成语义完整的段落,再合并到接近chunk_size的长度。overlap=50的意思是当前块末尾50个字符会复制到下一块开头,避免切点正好落在“杂草名称和防治方法”的分界处。参数上我建议chunk_size设400到600之间,太短则上下文信息不足,太长则向量检索的精度下降,500是多数RAG项目的默认值。
PDF解析有个血泪教训:get_text("text")输出的文本不一定按阅读顺序排列,尤其是双栏排版的文档,左栏和右栏会交叉。遇到这种情况,先用pdfplumber按坐标提取文本块,按左上角坐标排序后再合并,否则切出来的chunk逻辑是乱的。还有表格区域,PyMuPDF会把表格拆成碎片,我一般用pdfplumber单独提取表格转成Markdown格式再塞回文本流。
切分之后就是embedding入库:
import json def build_knowledge_base(pdf_path: str, index_path: str): text = extract_pdf_text(pdf_path) chunks = chunk_text(text, chunk_size=500, overlap=50) records = [] for idx, chunk in enumerate(chunks): vec = embed(chunk) # 复用前面定义的embed函数 records.append({ "chunk_id": idx, "chunk": chunk, "vector": vec }) # 向量不进JSON,单独用npy存,避免文件过大 vectors = [r.pop("vector") for r in records] np.save(index_path + ".npy", np.array(vectors)) with open(index_path + ".json", "w", encoding="utf-8") as f: json.dump(records, f, ensure_ascii=False)向量和元数据分开存是我惯用的做法,因为npy文件加载比JSON快一个量级,几万条向量用numpy矩阵乘法做检索只要几十毫秒。如果以后数据量大了再平滑迁移到FAISS或SQLite-Vec,接口不用改。
3.3 第二步:写一个实体抽取与检索召回的最小脚本
把前面两块拼起来,做一个最小可用的查询链路。这里不设计完整问答,只做“给定提问,召回相关chunk”这一步,目的是先验证PDF切分和embedding的质量。
import numpy as np import json class WeedKnowledgeBase: def __init__(self, index_path: str): with open(index_path + ".json", encoding="utf-8") as f: self.records = json.load(f) self.vectors = np.load(index_path + ".npy") def search(self, query: str, top_k: int = 5): q_vec = np.array(embed(query)) # 向量矩阵归一化后直接用点积,等价于余弦相似度 scores = self.vectors @ q_vec / ( np.linalg.norm(self.vectors, axis=1) * np.linalg.norm(q_vec) ) top_indices = np.argsort(scores)[::-1][:top_k] results = [] for idx in top_indices: results.append({ "chunk": self.records[idx]["chunk"], "score": float(scores[idx]), "chunk_id": self.records[idx]["chunk_id"] }) return results # 用法 kb = WeedKnowledgeBase("weed_kb") related = kb.search("水田里叶子像稻苗但分蘖很多的草", top_k=3) for r in related: print(r["chunk_id"], round(r["score"], 3), r["chunk"][:80])这里有个参数值得注意:top_k=3用于“召回给生成模型”,top_k=5用于“人工检查检索质量”。线上如果发现方案生成得不好,先别急着改prompt,把top_k调大看一眼召回的chunk到底有没有相关内容。很多时候问题根本不在生成,而在召回——这个顺序搞反了会浪费大量调prompt的时间。
4. 从“识别出草名”到“给出防除方案”:生成链路的设计与结构化输出
4.1 防除方案生成的推荐Prompt模板
检索召回只是前半程,后半程是让DeepSeek生成一份能落地的生态防除方案。这里的prompt设计比实体抽取讲究得多,因为生成任务有开放性,模型容易编造不存在的防除方法。我的做法是把“参考资料”放到“用户问题”前面,并且在输出schema里增加missing_info字段,让模型在信息不足时明说缺什么,而不是硬给方案。
PLAN_PROMPT_TEMPLATE = """ 你是植保专家,擅长杂草绿色防除。根据下面的参考资料回答问题。 参考资料: {retrieved_context} 当前场景: {user_query} 请严格按以下JSON结构输出: { "weed_name": "识别出的草种,中文俗名+拉丁学名", "identification_basis": ["识别依据,引用参考资料中的具体形态特征"], "hazard_level": "低/中/高", "eco_control_methods": [{"method": "非化学防除方法", "season": "适用季节", "note": "注意事项"}], "chemical_control_methods": [{"agent": "药剂", "dosage": "用量", "caution": "注意"}], "ecological_impact": "该方案对周边生态的影响评估", "monitoring_advice": "防除后的监测建议", "missing_info": ["参考资料中缺少的信息,如果没有则留空数组"] } 要求: 1. 只依据参考资料回答,参考资料里没有的内容一律写到missing_info里 2. 药剂名称与用量以当地登记为准,不要杜撰 3. 优先推荐物理、生物、生态等非化学防除手段 """模板里有几个设计点需要展开说。第一,“参考资料”放在“用户问题”之前,是让模型先读事实再回答问题,减少它调用自身训练知识的冲动。第二,“优先推荐非化学防除手段”这句话直接决定了输出风格——生态防除方案的核心价值就在这,如果不写这句,模型大概率直接给化学除草剂配方。第三,missing_info字段是关键的反幻觉机制,它把“我不知道”变成了合法输出,实测能让方案生成里的编造率明显下降。
4.2 结构化输出的JSON Schema与后处理校验
Prompt里定义了JSON结构,但模型输出的JSON偶尔会带前后缀说明文字,或者字段名拼写漂移。我一般用pydantic定义输出模型,解析失败时做一次纠错重试,这是最有效的“后悔药”。
from pydantic import BaseModel, Field from typing import List, Literal class EcoControlMethod(BaseModel): method: str season: str note: str = "" class ChemicalControlMethod(BaseModel): agent: str dosage: str caution: str = "" class WeedControlPlan(BaseModel): weed_name: str identification_basis: List[str] hazard_level: Literal["低", "中", "高"] eco_control_methods: List[EcoControlMethod] chemical_control_methods: List[ChemicalControlMethod] ecological_impact: str monitoring_advice: str missing_info: List[str]import json def parse_plan_response(raw_text: str) -> WeedControlPlan: try: data = json.loads(raw_text) except json.JSONDecodeError: # 模型偶尔会在JSON前后加解释文字,截取{}区间重试 start = raw_text.find("{") end = raw_text.rfind("}") if start == -1 or end == -1 or end <= start: raise ValueError("无法从模型输出中提取JSON") data = json.loads(raw_text[start:end+1]) return WeedControlPlan(**data)这段兜底逻辑的实际体验是:第一次JSON解析失败的比例大概在10%左右,截取{}区间后能救回一半;还失败的话,把错误信息拼接回原始prompt让模型重写一次,基本能稳定输出。注意WeedControlPlan(**data)这一步如果字段缺失会抛ValidationError,所以生产环境里我会再加一层默认值填充,保证解析失败时返回一个带missing_info的降级方案,而不是直接让接口报错。
4.3 生态防除方案的兜底规则:大模型之外的第二道保险
大模型对“检疫性杂草”“禁用农药”这类硬约束并不敏感,它在训练数据里读过“豚草要防除”的片段,但不一定知道“豚草是外来入侵检疫对象,发现需要上报”。这一类规则不适合靠prompt约束,而应该在模型输出后面挂一道规则引擎。
RULE_OVERRIDES = { "豚草": { "quarantine": True, "append_actions": ["发现后立即向当地植保部门上报"] }, "水花生": { "spread": "水陆两栖", "append_actions": ["避免机械切割导致断株扩散"] }, "菟丝子": { "append_actions": ["寄生植物,清除时要连根带藤一并移除"] } } def apply_rule_overrides(plan: WeedControlPlan) -> WeedControlPlan: weed = plan.weed_name if weed in RULE_OVERRIDES: rule = RULE_OVERRIDES[weed] if rule.get("append_actions"): # 把规则追加到生态防除方案的首位 eco = plan.eco_control_methods for action in rule["append_actions"]: eco.insert(0, EcoControlMethod(method=action, season="全年")) plan.eco_control_methods = eco return plan这个设计的核心是“生成后覆写”而不是“生成前约束”。因为prompt模板里把这些写死会让模型输出变得僵硬,而规则引擎可以在不调整Prompt的情况下,针对特定高风险草种强制追加动作。规则库的维护成本很低,植保人员每发现一个新需要重点管控的草种,加一条字典即可。我在实际项目里会把这类规则挂在实体归并之后,因为“豚草”的别名“豚草属”不会进这个字典,必须先归一化再匹配——这个细节当初调了好久才想明白。
5. 落地避坑:DeepSeek方案在真实杂草数据上翻车的5个高频问题
5.1 现象:识别出的草名是“禾本科杂草”这种泛化说法,方案生成直接跑偏
出现这个现象,十有八九是实体抽取阶段就出了问题。实体抽取时的system prompt没有明确禁止上位概念,模型看到“叶片条形,圆锥花序”就偷懒输出“禾本科杂草”。问题是后续所有检索都以这个草名为索引,泛化名称会把几页甚至几章的无关内容全召回来。
原因是抽取阶段的schema设计不严,只写了“杂草名称”,没限定粒度。解决方法是双管齐下:一是在system prompt里明确写“weed_name必须是具体种名,禁止使用科属等上位概念代替”;二是做实体归并时建一个“泛化词黑名单”,命中黑名单的抽取结果直接丢弃,让该chunk重抽一次。实测加入黑名单后,抽取的合格率能从70%出头涨到90%以上,这个提升完全靠规则,不换模型也能做到。
5.2 现象:语义检索召回的Top-3全是同一个属的重复内容,方案生成没有区分度
539页PDF里,同一杂草往往在不同章节反复出现:形态描述一章、危害发生一章、防治方法一章。向量库里有大量近似向量,检索“稻田杂草怎么防”时,Top-3很可能全是“稗草”在不同章节的复述,而用户真正问的可能是旁边的“野慈姑”。
原因是纯向量检索只算相似度,不管结果多样性。解决思路有两个:一是做混合检索,BM25和向量召回的chunk合并后去重,优先保留chunk_id跨度大的结果;二是引入最大边际相关性做重排序。最大边际相关性的核心是“挑一个跟query相关、但跟已选结果不那么相似的新结果”,代码很短:
def mmr_search(kb, query_vec, candidates, lambda_val=0.7, top_k=3): # candidates是已经按相似度排序的前20个候选 selected = [] remaining = candidates[:] while len(selected) < top_k and remaining: best_idx = None best_score = float("-inf") for i, cand in enumerate(remaining): sim_to_query = cand["score"] sim_to_selected = max( (np.dot(cand["vector"], s["vector"]) for s in selected), default=0.0 ) mmr_score = lambda_val * sim_to_query - (1 - lambda_val) * sim_to_selected if mmr_score > best_score: best_score = mmr_score best_idx = i selected.append(remaining.pop(best_idx)) return selected[:top_k]lambda_val这个参数我习惯设为0.7,意思是相似度权重占七成、多样性占三成。想多去重调到0.8,想多保留多样性调到0.6,原理不复杂,跑两轮人工看效果就能定下来。
5.3 现象:拉丁学名识别出来全是乱码,实体抽取的JSON里一片残缺
这个坑几乎每个做PDF知识库的人都会踩。PDF里拉丁学名通常用斜体排版,get_text("text")提取时,斜体字符经常和相邻文本错位,导致“Echinochloa crus-galli”变成“Echinochloacrus-galli”或者中间插入乱码。更麻烦的是有些PDF用嵌入字体,文字复制出来根本是编码错误。
我现在的处理方案是:识别阶段不指望拉丁学名,把“中文俗名+科属”作为主键;抽取结果里的拉丁名只做展示用,不参与检索匹配。再配合一个清洗正则,把常见的乱码字符过滤掉:
import re def clean_latin_name(raw: str) -> str: # 只保留拉丁字母、空格和常见标点 cleaned = re.sub(r"[^A-Za-z\s\.]", "", raw) # 压缩多余空格 cleaned = re.sub(r"\s+", " ", cleaned).strip() # 规范大小写:属名大写,种加词小写 parts = cleaned.split(" ") if len(parts) >= 2: return f"{parts[0].capitalize()} {parts[1].lower()}" return cleaned这个函数不能完全解决乱码问题,但能把可用的拉丁学名尽量救回来。遇到嵌入字体导致的不可读文本,唯一靠谱的办法是转OCR,用PaddleOCR或Tesseract单独跑含拉丁名的那几页,不要整本OCR,成本和错误率都不可控。
5.4 现象:防除方案和当地作物、气候完全不搭,北方果园推荐了热带轮作
这是典型的“缺少上下文约束”引发的幻觉。用户只问“这草怎么除”,模型不知道用户在江西还是辽宁,种的是水稻还是苹果树,于是按训练数据里的平均印象给方案。表面看方案逻辑完整,实际落地就是错的。
原因是query侧没有注入场景信息,检索侧也没有按地域过滤。解决方法是做“查询信息增强”:在把用户query送入检索之前,先拼接一个固定的场景前缀,比如“作物类型:水稻;地区:江西;季节:7月”。不需要让用户输入,系统在界面层把默认值拼进去就行。检索chunk时,如果PDF里有地域或作物信息,就按这个字段过滤一遍,优先召回同区域内容。
这个改动很小,但对方案可用率的提升非常明显。我做过一次对比测试,加入地域约束后,人工评审“方案在当地可用”的比例从不到一半提升到八成左右。关键点是不要指望模型自己想起来问用户地理位置,它不会问,它只会编。
5.5 现象:DeepSeek API调用延迟高、token开销超出预算,系统越用越贵
上线初期最常犯的错是:不管什么问题,都把整本书的相关内容全部塞进prompt,结果一次问答消耗几万字token。539页的PDF如果一次请求带进去,按token计费算下来单次成本就失控了,延迟也高得没法用。
正确的成本控制姿势是分层设计。实体抽取离线批次跑,控制并发,跑完存库就不花钱了;线上每次请求只带Top-K个检索chunk,top_k=3时上下文大概一两千字,加生成输出,单次成本可以压到几分钱甚至更低。另外一定要做缓存,相同或相似query直接返回上次生成结果,我见过实际项目加缓存后API费用降到原来的三分之一。
还有一个容易被忽视的点:DeepSeek本身有token用量统计,要按天盯“每请求平均输入token”这个指标。一旦发现这个数涨了,多半是代码里把整个chunk列表直接拼进了prompt,而不是按top_k截断。部署频率高的话,建议直接上vLLM本地部署DeepSeek开源模型,API费用变成GPU电费,量大的时候更划算。
6. 进阶验证技巧:用测试集给识别与方案生成做一次“体检”
做了这么多,不验证等于白做。我的做法是建一个“种子问题集”,不用多,20到30条就够,覆盖不同科属、不同危害等级、不同作物场景。每条包含三样东西:用户query、期望抽出的实体、期望方案里出现的关键动作。比如:
GOLDEN_SET = [ { "query": "稻田里叶片带毛、分蘖很多的杂草是什么", "expected_weed": "千金子", "expected_actions": ["排水", "晒田"] }, { "query": "果园里菟丝子缠绕果树怎么办", "expected_weed": "菟丝子", "expected_actions": ["连根清除", "避免断株扩散"] } ]验证指标分两套。实体级指标用自动脚本算:模型输出的草种名和期望草种名做匹配,算精确率、召回率、F1分数。方案级指标必须靠人工评审——让植保相关人员给生成方案打“可用/不可用”二分标签,或者1到5分打分。自动脚本这边,实体匹配会写得很直接:
def entity_f1(predicted: str, expected: str) -> float: pred_set = set(predicted.split("、")) exp_set = set(expected.split("、")) if not pred_set or not exp_set: return 0.0 precision = len(pred_set & exp_set) / len(pred_set) recall = len(pred_set & exp_set) / len(exp_set) if precision + recall == 0: return 0.0 return 2 * precision * recall / (precision + recall)这套种子集最有价值的用法是回归测试。每次改Prompt、换embedding模型、调整chunk参数之前,先把种子集跑一遍,记录F1和人工可用率;改完再跑一遍,对比差异。我踩过最大的坑是改了一版Prompt后整体效果看似更好,但原有20条里有一条“菟丝子”方案从“连根清除”被改成了“喷除草剂”——人工评审发现后才知道是Prompt里加了句“优先推荐非化学手段”被模型理解为“可以提化学手段”,如果把回归测试纳入流程,这种退化在发布前就会被拦住。
现在我的习惯是:实体抽取、语义检索、方案生成这三个环节各自建一套独立的种子集,每次改动只跑对应环节的测试,避免“改检索却要跑全流程人工评审”的低效循环。这套验证方法听起来朴素,但比任何花哨的评估框架都实用——它能让你在别人问“这个方案靠谱吗”的时候,直接掏出一个数字出来说话。希望帮到你。
本文还有配套的精品资源,点击获取