简介:这份554页PDF文档围绕DeepSeek对话管理框架,系统讲解法律智能助手从技术选型到落地部署的全链路构建方案,面向NLP工程师、算法工程师、法律科技产品经理及对话系统开发者,旨在解决多轮法律咨询中的上下文理解与精准应答难题。资源共1个PDF文件,压缩包14.38MB,内含50个大章节,支持目录跳转与书签大纲,读者可依据具体章节快速定位技术方案。内容覆盖法律专业词汇库搭建、实体识别与意图识别的数据标注规范、基于DeepSeek框架的模型训练参数设置、微调策略与蒸馏部署等核心环节,同时剖析了长对话上下文衰减、法律实体指代消解、模糊意图迁移等实务难点,并给出可复用的解决思路。文档结构完整,从需求拆解、技术选型、数据工程到模型优化层层递进,辅以标注规则、训练流程和参数配置,既适合作为项目建设的路线图,也可作为技术评审的参考资料。已有108人学习,对于正在构建法律智能化应用或希望借鉴垂直领域对话系统完整方案的开发者,具有较高的参考价值。 用户问"我在工厂干了三年,公司一直没签合同,现在把我辞了,能要赔偿吗?"——第一轮就把关键信息交代得差不多。但他追问"那加班费还能不能算?"时,很多系统已经忘了前面的"没签合同"和"被辞退"。DeepSeek法律智能助手对话系统要解决的,正是多轮法律咨询场景下的上下文理解与精准应答生成。它的做法是把"记住什么、追问什么、怎么回答"拆成三层:对话管理框架维护案件状态,DeepSeek负责抽取事实与生成答复,中间垫一层上下文压缩。这套方案适合正在做智能法律咨询、企业法务助手或律所接案预检的团队;如果只是让DeepSeek写几句法律文案,用不上这套东西,但一旦做多轮对话,状态管理就绕不过去。
2. 对话管理框架选型与状态建模:先给DeepSeek装一个"案件事实记账本"
做多轮法律咨询,第一个要回答的问题是:用户上一轮透露的关键信息,这一轮还在不在。有人图省事,把每轮聊天记录原样塞给DeepSeek,发现上下文窗口装不下之后就开始截断,一截断就"失忆"。所以圈内通行的做法,是先搭一个对话管理框架,用状态机持续记录对话内容。这个框架本质上是模型外围的调度层,它不让DeepSeek自由发挥"记什么",而是规定好哪些信息必须落盘。
2.1 三种对话管理框架的取舍:自研状态机、开源DMF与LLM编排引擎
我身边做法律助手的团队,基本在三条路线里选:
第一条是自研轻量状态机。把法律咨询流程建模成明确的状态转移,每轮由状态机决定下一步追问什么,DeepSeek只负责抽取用户回答里的字段并生成话术。优点是控制力极强,用户说话再乱,系统也不会乱问;缺点是状态转移表要自己维护,前期工作量大一点。
第二条是用Rasa这类开源对话管理框架。槽位、故事、NLU组件都是现成的,但接DeepSeek时要把框架内置的意图识别换成外部API调用,适配层反而要写不少代码。更麻烦的是,法律咨询里"事实收集"和"法律分析"混在一起,用户不按顺序出牌,Rasa的故事模板很难覆盖真实会话,标注成本很高。
第三条是用LangGraph这类LLM编排引擎,把理解和生成全交给模型。灵活是真灵活,但法律场景要审计、要可控,编排引擎的决策路径是一个黑匣子,出了责任问题不好回溯。用户问"我的案子能赢吗",这类框架很容易顺着话题开始编胜诉率。
| 维度 | 自研状态机 | Rasa | LLM编排引擎 |
|---|---|---|---|
| 状态可控性 | 高 | 中高 | 中 |
| 多轮策略自定义 | 灵活 | 受故事模板约束 | 灵活 |
| 接入DeepSeek成本 | 低 | 中 | 低 |
| 出问题可回溯性 | 好 | 一般 | 差 |
| 适合场景 | 流程固定的法律咨询 | 标准客服任务 | 开放域Agent |
我一般会选自研轻量状态机,原因有三条:法律咨询的核心对话流程其实不复杂,就是"收集事实、澄清诉求、给结论、补追问";状态需要能导出和审计,自研状态下每个字段的变更来源都清晰;DeepSeek的接入成本很低,状态机只需要调它的接口做抽取和生成。这套自研的东西,说白了就是一个harness,把DeepSeek夹在业务规则和输出约束之间,不让它脱缰。
2.2 法律咨询状态建模:槽位、确认标记和事实链
状态建模是对话管理框架里最花心思的部分。法律咨询不是填表单,用户不会按顺序把"入职时间、离职时间、是否签合同"一条条报给你。他会先说结果,再补背景,中间还可能纠正自己。所以状态结构不能是一张死板的表,得能承载"说了什么、确认了什么、还没收集什么"三层信息。
from dataclasses import dataclass, field from typing import Dict, List, Optional, Set @dataclass class LegalDialogState: dialog_id: str case_type: str = "" # 案件类型:劳动仲裁/借贷纠纷/婚姻家事 user_role: str = "" # 用户身份:劳动者/用人单位/债权人 counterparty: str = "" # 对方当事人 key_facts: list = field(default_factory=list) # 关键事实,如"未签合同" claims: list = field(default_factory=list) # 用户诉求,如"要赔偿金" timeline: list = field(default_factory=list) # 时间线事件,按顺序追加 evidence: list = field(default_factory=list) # 用户提到的证据 confirmed: Set[str] = field(default_factory=set) # 已确认过的字段名逻辑说明:这个类是对话管理框架里的"案件事实记账本"。key_facts和timeline不是单值槽位,而是列表——因为法律咨询里同样一个重要事实可能分多次出现,比如"我签过合同"和后来补充的"合同是2022年签的",应该合并成一条完整记录,而不是互相覆盖。confirmed集合是专门为法律咨询设计的保护机制,用户经常先说一个信息再纠正,没有它,模型就会把后轮随口带过的话当成事实覆盖掉前面认真确认过的内容。
参数说明:case_type、user_role、key_facts、claims是核心必填槽位,生成回答前必须齐全;timeline用来保留事件时序,比如"先被辞退、后主张加班费"和"在职期间提仲裁"完全是两种法律路径;evidence专门记用户说到的证据,这一项在后续RAG检索法条时也会用到。confirmed不是给用户看的槽位,它是内部状态机用的。
槽位更新也不能无脑覆盖。用户说"我是2020年入职的……不对,是2021年",如果模型直接往后写,前面所有判断都会基于错误日期。所以更新逻辑要加一层确认锁:
def apply_slot_update(state: LegalDialogState, nlu_result: dict) -> None: corrected = set(nlu_result.get("corrected", [])) for field, value in nlu_result.get("slots", {}).items(): # 已确认字段只有用户明确纠正时才允许覆盖 if field not in state.confirmed or field in corrected: setattr(state, field, value) state.confirmed.add(field)逻辑说明:nlu_result是DeepSeek解析完用户本轮发言后输出的结构化结果,slots里面是它认为需要更新的事实字段,corrected里面是它判断用户正在纠正的旧字段。这里的核心逻辑一句话——没有confirmed保护的字段随便更新,已经被确认过的字段必须等用户明确纠正才解锁。
参数说明:corrected字段非常重要,它的判定标准写在NLU提示词里,要求解析器只有在用户说出"不对""其实""更正一下"这类信号时才返回。宁可漏判也不要误判,漏判只是信息更新慢一步,误判会把已经坐实的事实推翻,后面整段回答跟着乱。
2.3 DeepSeek在框架里的分工:NLU走JSON抽取,不让模型直接改状态
状态机搭好之后,接下来要解决DeepSeek怎么参与的问题。最忌讳的做法是让大模型直接改状态对象——模型输出不稳定,可能编一个不存在的字段名,也可能一次输出十个槽位把状态冲乱。我采用的分工方式是:DeepSeek做NLU,但只输出JSON,状态机拿到JSON之后校验合法再落盘。
from openai import OpenAI import json, os client = OpenAI( api_key=os.getenv("DEEPSEEK_API_KEY"), base_url=os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com"), ) NLU_PROMPT = """你是对话状态解析器,只做抽取,不做法律答复。 从用户输入中提取法律咨询相关的槽位更新。 可选字段:case_type, user_role, counterparty, key_facts, claims, evidence, corrected 必须输出 JSON,格式: { "intent": "clarify_fact | answer_question | change_topic | provide_request | ask_followup", "slots": {"字段名": "更新值"}, "corrected": ["被用户纠正的字段名"] } 用户没有提供新信息时,slots 返回空对象。""" def parse_turn(user_input: str, dialog_id: str) -> dict: resp = client.chat.completions.create( model="deepseek-chat", messages=[ {"role": "system", "content": NLU_PROMPT}, {"role": "system", "content": f"当前对话ID:{dialog_id}"}, {"role": "user", "content": user_input}, ], temperature=0, response_format={"type": "json_object"}, ) return json.loads(resp.choices[0].message.content)逻辑说明:parse_turn是状态机与DeepSeek之间的唯一数据出入口。模型只负责理解用户这句话表达了什么意图、更新哪些槽位、纠正了哪些旧信息,不负责决定下一步对话走向。状态机拿到返回的JSON后,先检查字段名是否在预定义列表里,再走apply_slot_update落盘。这样即使模型某次抽取出错,也只会错一个字段,不会把整个状态搅乱。
参数说明:temperature必须写死为0,NLU阶段任何随机性都不能容忍。response_format指定json_object,让模型输出严格JSON,省去正则提取的麻烦。intent的取值是封闭集合,状态机会按这五种意图分流:clarify_fact时进入追问流程,answer_question时进入生成流程,change_topic时清空本轮临时状态,provide_request时更新claims,ask_followup时把用户的追问追加为新的需要澄清的事项。
这种分工带来的最大收益是可复盘。对话出问题时,打开状态机的日志,能清楚看到哪一轮、哪个槽位、由哪条用户输入触发更新。DeepSeek在里面只是一个"翻译官",把口语翻译成结构化的状态变更,决策权始终握在自己手里。
3. 多轮上下文理解:把七零八落的聊天记录整理成一条事实链
状态机解决了"关键信息不丢",但模型要生成好回答,还需要看到上下文顺序。用户是先被辞退再主张加班费,还是在职期间就提了仲裁,这些时序在槽位里看不出来。所以上下文理解要做的事情,是在状态机之外维护一条"事实链",把聊天记录变成可回溯、可压缩的素材,而不是一坨越攒越长的流水账。
3.1 上下文窗口是硬约束:三层记忆而不是一条流水账
DeepSeek的上下文窗口虽然不小,但法律咨询用户经常贴合同原文、贴仲裁裁决书,一贴就是几千字。加上前面十来轮对话,一次请求轻松冲破模型的处理预算。更现实的问题是,长上下文的注意力会被稀释,模型看到第8000个token时,早就忘了第200个token里那句"我没签合同"。
所以我在这个项目里用的是三层记忆,而不是简单地把所有历史塞进去。
| 记忆层 | 内容 | 上限 |
|---|---|---|
| 状态层 | 槽位快照+confirmed标记 | 固定,约200 token |
| 摘要层 | 被挤出窗口的旧轮压缩摘要 | 控制在400 token |
| 滚动层 | 最近4~6轮原文 | 原文保留,约1500 token |
状态层每轮必带,它是系统记忆的主干;滚动层保留最近几轮原文,让模型能看到最近的语气和未尽事宜;摘要层兜底,把更早的历史压成事实链。三层一起拼进请求,既控成本,又保证关键事实永不被挤出去。只做滚动窗口是很多项目翻车的根源,因为用户在第2轮提到的关键信息,到第10轮早就被冲走了。
3.2 法律口语的指代消解与归一化:抽槽时就把"他"翻译出来
法律咨询的口语化程度远超预期。用户不会说"用人单位",只会说"老板""公司""他们那边";不会说"相对方",只会说"对方"。如果不做归一化,状态里会出现"老板"和"公司"两个不同的实体,模型在后续判断时会把人搞混。
这里的常见做法是在NLU阶段顺带做实体归一化,让DeepSeek在抽槽时就把指代展开:
ENTITY_NORMALIZE_RULES = """ 实体归一化规则: - 老板/公司/单位/他们(工作场景)→ 用人单位 - 员工/我/本人 → 劳动者(当user_role为空时) - 中介/劳务公司/外包 → 第三方用工单位 - 法院/仲裁委 → 裁判机构(除非用户特指某一家) """ def parse_turn(user_input: str, dialog_id: str) -> dict: messages = [ {"role": "system", "content": NLU_PROMPT + ENTITY_NORMALIZE_RULES}, {"role": "system", "content": f"当前对话ID:{dialog_id}"}, {"role": "user", "content": user_input}, ] resp = client.chat.completions.create( model="deepseek-chat", messages=messages, temperature=0, response_format={"type": "json_object"}, ) return json.loads(resp.choices[0].message.content)逻辑说明:这版parse_turn在原来的NLU提示词后面拼接了实体归一化规则。关键点在于,归一化发生在状态更新之前,而不是生成回答的时候。如果拖到生成阶段再改,模型输出里就会跟着用户说"老板不给我钱",整个回答的法言法语风格就垮了。
参数说明:归一化规则要根据业务领域维护。劳动法场景维护一套映射,婚姻家事又是另一套,比如"老公/老婆/对象"要归一成"配偶"。这类规则不需要穷举,抓到出现频率最高的前20个口语称呼就够了,剩下的让DeepSeek根据上下文理解后填一个标准名称进槽位。
3.3 一个可落地的上下文压缩实现:摘要函数
压缩这条事实链不能简单地截断,截断等于丢失。我的做法是让DeepSeek自己对旧历史做一次摘要,再把摘要作为一条system消息拼回上下文。这样旧信息不是被删掉,而是被"提炼"了。
def compress_history(history: list, max_rounds: int = 6, llm_client=None) -> list: if len(history) <= max_rounds: return history keep_recent = max_rounds // 2 older = history[:-keep_recent] recent = history[-keep_recent:] compress_prompt = ( "你是法律对话摘要器。把下列对话历史压缩成事实链摘要,要求:\n" "1. 只保留用户陈述过的事实、时间和诉求,不要推断结论;\n" "2. 保留对话里的更正信息,例如'更正:入职时间是2020年';\n" "3. 输出不超过200字的连续文本,不要用列表。\n\n" "对话历史:\n" + json.dumps(older, ensure_ascii=False) ) resp = llm_client.chat.completions.create( model="deepseek-chat", messages=[{"role": "user", "content": compress_prompt}], temperature=0.2, max_tokens=400, ) summary = resp.choices[0].message.content ctx = f"对话历史摘要(早于近{keep_recent}轮):{summary}" return [{"role": "system", "content": ctx}] + recent逻辑说明:这个函数把历史切成两段,前半段压缩成摘要,后半段保留原文。返回结果里第一条是一条system消息,后面是最近几轮原文。放在system位置是因为摘要的优先级最低,模型会优先参考后面靠近用户输入的原文,摘要只作为背景信息,避免摘要里的某句话被模型误当成最新指令。
参数说明:max_rounds取6比较均衡,保留最近3轮原文,其余全压。如果业务上需要更强的连贯性可以调到8,但要付出更多token成本。摘要的temperature设0.2,比NLU稍高一点,允许摘要语言有一点变化,但绝不能高到0.5以上,否则摘要会开始加戏。提示词里那句"不要推断结论"是防幻觉的关键,不加这句,模型经常把"用户没签合同"推断成"单位违法",摘要就带偏了。
4. 精准应答生成:用参数、模板和状态把DeepSeek的回答约束在法律边界内
上下文理顺了,下一步是精准应答生成。这一步的"精准"有两层意思:一是事实准确,回答必须基于已确认的槽位,不能张冠李戴;二是表达合规,不能给用户承诺结果,不能编造法条。两者单靠模型自觉都做不到,得靠参数、提示词和生成后校验一起压。
4.1 法律场景的生成参数:temperature、top_p和max_tokens怎么设
| 参数 | 推荐值 | 作用 | 反例 |
|---|---|---|---|
| temperature | 0.1~0.3 | 控制随机性 | 0.8时同一问题两次给出相反结论 |
| top_p | 0.85~0.95 | 缩小候选词池 | 过高时冒出罕见表达 |
| max_tokens | 800~1200 | 限制回答长度 | 太长时车轱辘话来回说 |
法律咨询场景必须用低温和低top_p,原因是用户要的是可复现的答案。同一问题上午问和下午问,结论应该一致;换一种问法,核心判断也不能变。客服场景可以把temperature调到0.8让回复显得更活泼,法律场景这么做就是事故。
我遇到过真实翻车案例:早期接DeepSeek时temperature设了0.7,用户同一句"我这种情况能要赔偿吗"连问两次,第一次回答"可以主张经济补偿金",第二次变成"需要看合同约定,建议协商解决"。用户直接截图投诉说系统胡扯。压到0.2之后,这类随机漂移基本消失。
调参时还有个实用技巧:用开发工具接DeepSeek跑实验。常见做法是在Codex或VS Code里按OpenAI兼容接口配置好DeepSeek的端点,改一版prompt跑一版样例,比在Web页面里反复手测效率高得多。参数和提示词是配套调的,只改参数不改提示词,很难看出真实效果。
4.2 三段式提示词模板与结构化输出:结论、依据、追问
提示词模板是精准应答的主战场。法律咨询的回答不能是自由散文,我给DeepSeek定的模板是固定三段:结论、依据、行动建议。三段都不齐就不算一次合格回答。同时要求缺失关键事实时,模型必须先追问,不能硬答。
SYSTEM_PROMPT = """你是法律咨询助手DeepSeek,回答必须遵守: 1. 结构固定为三段:结论、法律依据、行动建议; 2. 结论必须基于已确认的案件事实,关键事实缺失时不要下结论,改为追加提问; 3. 法律依据只写法律规定名称;记得条文号可以写,不确定时必须笼统表述; 4. 禁止承诺性表述,如"肯定能赢""胜诉率百分之XX"; 5. 输出JSON,字段为 conclusion, legal_basis, actions, risk, followup_questions。 当前案件事实快照:{state_snapshot} 对话历史摘要:{history_summary}"""JSON输出schema固定如下,状态机会按这个结构解析:
{ "conclusion": "可以主张经济补偿金", "legal_basis": ["《劳动合同法》相关条款"], "actions": ["收集工资流水和解除通知", "向劳动仲裁委员会申请仲裁"], "risk": "仲裁时效可能已过,建议尽快启动", "followup_questions": ["你入职时是否签订过书面合同?"] }逻辑说明:JSON结构不是给用户看的,是给工程层用的。前端拿到conclusion渲染成正文,拿到legal_basis渲染成引用样式,拿到risk弹一个风险提示条。更关键的是它给了生成后校验一个抓手——如果legal_basis是空数组,系统就知道模型没找到依据,要走人工介入流程;如果risk缺失,说明模型漏了风险提示,直接拒绝渲染。
参数说明:followup_questions是保证多轮质量的关键字段,模型每次回答都要带上下一轮该追问的问题。这样对话不会在用户回一句"好的"之后就冷场。提示词里"不确定时必须笼统表述"这句话是防法条幻觉的第一道防线,写得越死,模型越不敢编条文号。
4.3 完整的多轮应答生成实现:状态驱动而不是聊天记录驱动
把前面的模块串起来,就是一个可以直接跑的最小实现。它和"把历史聊天记录全丢给DeepSeek"的核心区别在于:生成用的上下文是"状态快照+摘要+最近对话",不是单纯的聊天流水账。
def handle_turn(state, history, user_input): # 第一步:NLU解析并更新状态 nlu = parse_turn(user_input, state.dialog_id) apply_slot_update(state, nlu) # 第二步:缺关键事实时优先追问,不让模型硬答 if not state.confirmed.issuperset({"case_type", "user_role", "key_facts", "claims"}): missing = [f for f in ["case_type", "user_role", "key_facts", "claims"] if f not in state.confirmed] return build_clarify_question(missing, state) # 第三步:组装三层上下文 messages = [ {"role": "system", "content": SYSTEM_PROMPT.format( state_snapshot=json.dumps(state.__dict__, ensure_ascii=False), history_summary=summarize_or_load(history) )}, {"role": "user", "content": user_input}, ] # 第四步:调用DeepSeek生成结构化答案 resp = client.chat.completions.create( model="deepseek-chat", messages=messages, temperature=0.2, top_p=0.9, max_tokens=1000, response_format={"type": "json_object"}, ) answer = json.loads(resp.choices[0].message.content) # 第五步:生成后合规校验 answer = post_check(answer, state) return answer逻辑说明:这个函数把前面的状态机和NLU全部串起来了。第一步让DeepSeek看懂用户这句话并更新记账本;第二步是"不硬答"策略,四个必填槽位没集齐就走澄清分支,宁可多问一句也不给半吊子结论;第三步组装上下文,历史摘要和状态快照都在system层;第五步的post_check在返回前端之前把答案过一遍合规规则。
参数说明:temperature和top_p沿用了4.1的推荐值,max_tokens设1000是为了给三段式回答留够空间,同时避免无限输出。第二步的澄清分支是精准应答的精髓——很多团队把"精准"理解成"答案精准",其实在信息不全时,"精准地追问"比"精准地瞎猜"更重要。post_check内部做三件事:检查legal_basis数组是否为空、用正则扫一遍"胜诉率""肯定能"这类词、确认risk字段存在。任何一项不过,就追加对应提示后重新生成一次,或降级到人工话术。
5. 避坑与排查:法律对话系统上线前最容易翻车的5个问题
这一章写的都是这个类型项目里反复出现的问题,每条按"现象、原因、解决"拆开,基本都能在你自己的系统里复现一遍。
5.1 第二轮就"失忆":让模型背历史是偷懒方案
现象:用户第一轮说"我在工地摔伤",第二轮问"我可以报工伤吗",回答里完全不提工地,甚至按交通事故给了一通建议。
原因:实现时只把最近两轮聊天记录塞给模型,早期轮次的关键事实已经不在上下文里;或者压缩摘要时丢了关键实体。
解决:关键事实在NLU阶段就必须写进槽位,并在每轮system里注入"案件事实快照"。聊天记录只是参考,槽位才是主记忆。血泪经验:早期版本只靠滚动窗口,用户提到的重要信息一旦被挤出窗口,系统就开始胡说。做了状态层之后,"失忆"问题基本绝迹。
5.2 法条幻觉:模型编出一个不存在的条文号
现象:回答里出现"根据《劳动合同法》第九十八条",实际该条是别的主题,用户一查就露馅。
原因:大模型的参数记忆不可靠,条文号是最容易被幻觉化的部分,训练数据里的法条差异和更新会让模型记串。
解决:三层兜底。第一层,提示词要求不确定时不写条文号;第二层,生成后做规则校验,用正则匹配"第.*条",在本地法条库里查不到就替换成法律名称;第三层,有条件时接法条检索接口,把命中的候选条文原文送进模型,让模型基于原文引用而不是凭记忆。
5.3 追问偏航:用户说"我不是这个意思"
现象:系统像审问一样连问三四个问题,用户不回答,或者回答一句后系统发现完全理解偏了。
原因:意图识别是单轮的,状态机没有"澄清-回退"机制;槽位更新策略太激进,把用户随口带过的话当成新事实覆盖了旧事实。
解决:在NLU输出里增加一个confusion意图。DeepSeek判断用户回答与问题无关时,状态机先触发澄清而不是更新槽位。同时给每个槽位加confirmed确认标记,只有用户明确说"不对""其实"才解锁覆盖。confusion意图的prompt写法很简单:"如果用户没有回答你的问题,而是说了其他内容,intent返回confusion"。
5.4 Token成本失控:请求体越来越胖
现象:对话到第15轮时,每次请求要带上全部历史,响应时间从1秒涨到3秒,账单额度肉眼可见地往下掉。
原因:没有压缩策略,历史记录只增不减,每轮都送全量上下文。法律场景里用户还会贴长文本,翻车更快。
解决:用第3章的compress_history做分层压缩。我踩过这个坑之后给自己定了一条硬规则——单次生成请求的token预算控制在2000以内,超了就把历史交付给摘要层。最近6轮保留原文,更早的全部提炼。这样既保住了关键事实,又控制住了费用。
5.5 合规红线:模型承诺了"胜诉率"
现象:用户问"我能不能赢",模型回答"胜诉率90%"。这在法律行业是严重合规事故,律师都不敢这么打包票。
原因:prompt里没有边界设定,产品上也没有免责兜底。模型在生成时顺着用户的话头往下滚,越滚越具体,最后落到一个本来就不该给的数字上。
解决:系统提示词显式禁止承诺性表述,结构化输出的risk字段做成必填。UI侧固定展示"本回答不构成正式法律意见"的免责声明。如果接入了RAG,要把这条免责声明固化成最高优先级的system约束,任何生成前都注入。校验脚本里加一条正则,匹配"胜诉率|一定能赢|稳赢"就拦截重生成。
6. 验证与进阶:用10个多轮场景评测你的法律助手,再向RAG演进
多轮对话系统的验证不能靠"看几个例子感觉还行"。我的做法是维护一套多轮场景评测集,每次改完prompt或状态机就跑一遍回归。
6.1 十场景评测集与回归验证
评测集按业务高频方向建了10组场景:劳动争议3个、借贷纠纷2个、婚姻家事2个、劳动报酬2个、侵权1个。每个场景设定一个"用户隐藏的真实诉求",标注每轮期望的追问方向、最终结论类型和必须被状态机记录的关键事实。跑回归的方式很简单,拿这10组对话从头跑一遍,对比输出满足度:
| 指标 | 计算方式 | 合格线 |
|---|---|---|
| 槽位准确率 | 每轮NLU提取槽位与标注一致的比例 | >85% |
| 关键事实召回 | 结束时状态机里关键事实占标注的比例 | >95% |
| 回答合规率 | 无承诺性表述且结构合法的回答比例 | 100% |
| 任务完成率 | 达成场景预设目标的对话比例 | >80% |
| 平均轮次 | 完成一个场景需要的对话轮数 | ≤8轮 |
这套评测跑一遍通常只要几分钟,但能挡住大多数隐蔽回归。尤其是改提示词后,你肉眼看着新版本更好,评测集却会告诉你某个场景的追问方向已经偏了。
6.2 向RAG与私有化部署演进
进阶方向是把这个方案接到RAG上。多轮法律咨询的RAG设计和普通知识问答有个关键区别:检索query不能用用户原话,要用状态快照生成。用户问"赔偿能要多少",但状态里记录的是"劳动仲裁、未签合同",那检索词就应该是"未签订劳动合同 经济补偿金 计算标准",这样才查得到对症的法条。
数据安全方面,法律咨询数据敏感,能私有化部署就私有化部署。DeepSeek支持本地部署,但小参数模型做法律分析能力有限,常见的分层方案是:本地小模型做槽位抽取和敏感信息脱敏,远程大模型做最终生成。这样既守住了数据安全底线,又保住了回答质量。
做这个方案给我最大的教训是:对话系统不能过度相信模型的临场发挥,法律咨询尤其如此。每次改完对话策略,我第一件事不是看演示例句,而是把那10个场景的回归脚本跑一遍,看有没有哪个场景因为一句prompt改动就偏了。这个习惯替我挡掉了好几次上线事故。希望帮到你。
本文还有配套的精品资源,点击获取