大模型智能办理:工具调用+策略编排的保险客服升级方案

发布时间:2026/10/21 3:39:06
大模型智能办理:工具调用+策略编排的保险客服升级方案
给某保司做智能客服升级时我最大的感受是只让大模型“对话”很容易但要做成大模型智能办理必须补齐两样东西——工具调用让模型能真正操作业务系统策略编排让所有业务规则能约束模型的动作。这套组合拳就是这次我想分享的完整方案。传统智能问答你可以把它理解成一个“嘴替”用户问什么它答什么但答完之后用户还是要自己去App里点按钮、翻页面、填表单。而“智能办理”要做的是把咨询和业务动作打通用户说一句“帮我看看这份保单能不能退”系统不只是告诉他退保规则而是直接帮他查出保单状态、测算退保金额、走完前置校验再给出下一步该提交哪些材料。看起来只是多走了一步实际上整个系统的架构逻辑完全变了。这篇文章我会从系统边界设计、工具调用层、策略编排层、核心源代码、实测踩坑五个方面展开。适合正在做智能客服升级、保险中介平台、金融合规强约束对话系统的朋友参考。我尽量把代码和配置都贴出来你可以直接照着搭一个最小可用版本再往自己的业务域扩展。1. 从“问答”到“办理”保险智能服务的真正分水岭1.1 用户问了几十个问题最后还是没办成事先说说我看到的真实情况。项目上线之前团队对存量客服会话做了一次抽样分析发现一个扎心的数据接近一半的会话用户在问完“能不能办”“怎么办”之后没有得到任何可执行的结果而是被引导去App自己操作。我们的埋点统计里真正在跳转之后完成下一步操作的用户占比不到四分之一。为什么会这样因为原来的系统本质是一个“检索生成”管道。模型从知识库里找到保险条款、产品说明、退保规则然后生成一段流畅的回复。用户问“退保扣多少钱”它能把计算公式和注意事项讲得清清楚楚但用户真正要的是一次退保测算甚至是一份退保申请草稿。这个需求普通问答架构给不了。所以后来我们内部达成了一个共识保险咨询这类场景问答是手段办理才是目的。用户带着问题来最后要的是一个结果——要么查到了具体数据要么发起了一个流程要么确认了一个方案。1.2 智能办理与智能问答的本质差异状态变更、权限与闭环问答系统是“只读”的模型读取知识、生成文本完事。智能办理是“会写”的它要调用接口、修改数据、发起流程。这带来了三个必须面对的问题。第一是状态变更。查询保单、试算保费是相对安全的读操作但退保申请、理赔预审、保单贷款这类动作会改变业务状态不能随便让模型碰。第二是权限。用户只能查自己的保单代理人可以帮客户试算内勤可以查看工单不同角色能看到的数据和能执行的动作完全不同。模型本身没有权限概念它只是根据提示词生成文本。第三是闭环。办理类操作必须留痕出了问题要能回溯每一步是谁触发的、基于什么条件、结果是什么都要能查。这三个问题叠加在一起就引出了我的核心判断不能让模型直接对接业务接口也不能让模型自由决定业务流程。需要在模型和业务系统之间加两层东西——工具调用层和策略编排层。1.3 为什么选“工具调用策略编排”而不是让Agent自由发挥现在很多团队上来就做Agent让模型自己规划、自己调工具。这在开放场景里效果很好但保险金融是强合规场景模型的自主规划能力越强越容易出问题。模型会为了完成用户请求而“创造”出不存在的流程或者漏掉某些前置校验而且它的决策过程很难稳定复现出问题之后不好追责。我的方案是把“手脚”和“大脑”分开。工具调用层解决的是模型能碰到哪些业务能力。它的本质是给模型一个白名单式的API集合模型只能从中选择不能自己发明调用方式。策略编排层解决的是这个业务到底允不允许这么办。模型可以提出意图和参数但流程怎么走、走到哪一步要停下来、什么情况下转人工由策略引擎说了算。这样设计的最大好处是可控。模型负责理解用户说什么、提取关键信息策略层负责判断“能不能办、怎么办”。两边各管一摊出问题了也能快速定位是模型理解错了还是策略配置不对。2. 工具调用层让大模型只通过白名单“接触”业务系统2.1 工具注册表业务能力的唯一入口与权限标注工具调用层的第一步是建立一张工具注册表。所有业务能力——保单查询、费率试算、投保预审、理赔材料清单、续期提醒——都封装成标准化的“工具”统一注册到一个Registry里。每个工具至少包含这几个要素名称、描述、参数Schema、执行函数、可用角色、是否敏感操作。模型发起工具调用时拿到的不是真实的Python函数引用而是一份符合JSON Schema格式的说明它会根据说明生成调用参数然后由注册表统一执行。这是“白名单”的关键。模型不会直接拼SQL、不会直接调内部REST接口它只能调用注册表里存在的工具。而且每个工具都有角色限制比如“查询他人保单”这个工具只有内勤角色能触发用户侧角色即使模型想调也会被注册表拦下。我见过一些方案把工具定义散落在各处模型提示词里写一部分代码里又硬编码一部分后期维护非常痛苦。用注册表统一管理之后新增一个业务能力就是注册一个工具的事模型侧的schema列表也会自动更新。2.2 函数描述怎么写一个坏描述如何让模型乱调工具工具调用的一个隐藏关键点是函数描述。很多人只写一句“查询保单信息”结果模型在用户问“什么是重疾险”的时候也去调了这个工具白费一次调用不说还把上下文搞乱了。我写过一个反例某个工具的description是“查询保单详情”实际测试中用户说“我买的保险是不是白交了”模型以为用户要查保单缴费记录立刻调用了查询接口结果因为用户没有登录接口返回空值模型只好又编了一段“暂时无法查询”的回复。整个过程看起来没毛病但实际上工具根本没有被正确使用。后来我把描述改成了这样仅当用户询问其名下具体保单的缴费状态、保障责任、保单利益时使用。如果用户没有登录、没有明确提到“我的保单”“我买的那份”等具体指向或者只是在询问保险概念和产品知识请勿调用本工具。若缺少保单号或身份证信息应通过追问澄清。这个改动之后误调用率明显下降。写工具描述的原则很简单告诉模型什么时候用更要告诉它什么时候不能用。参数描述也一样要写明口径比如年龄字段是周岁还是虚岁金额单位是元还是万元日期格式是什么。模型提取参数时对口径越模糊越可能填错。2.3 返回值裁剪与适配层别把整个保单JSON都丢给模型工具执行完之后返回结果也不能直接塞给模型。真实业务系统的保单详情可能有一百多个字段加上各种状态码、时间戳、内部标识如果全部放进模型上下文成本高而且会干扰模型判断——它可能被无关字段带偏回复里出现用户根本不该看到的内容。所以每个工具最好配一个适配器。工具执行函数返回原始结果适配器负责把它转换成“面向对话的摘要”只保留当前用户真正关心的字段并把内部状态码翻译成人类可读的文本。比如“status03”转换为“保单已失效”拒赔原因里的内部代码转换为具体说明。这样模型拿到的是干净、聚焦的信息生成回复的准确性和速度都会好很多。顺带还能省token——在金融场景里模型上下文长度就是钱几千个token的重复裁剪日积月累是一笔不小的成本。2.4 工具调用异常与重试参数错误不重试超时才重试工具调用一定会遇到异常关键是模型侧怎么感知异常。我统一封装了一个ToolResult结构包含四个字段success、data、error_msg、duration_ms。模型拿到的永远是这个结构的“安全摘要版”不会看到Python堆栈或内部日志。异常处理我遵循一个原则参数类错误不重试去澄清超时类错误最多重试一次业务类错误直接给替代方案或转人工。为什么参数错误不重试因为模型已经把参数提取错了重试大概率还是错不如让它追问用户。超时重试有价值因为可能是服务端瞬时抖动但也不能无限重试一次就够了再失败就转人工。还要注意一个细节工具执行失败后模型很容易“脑补”原因。比如工具返回“查无此单”模型可能会说“您的保单已失效”这种推理但实际上只是数据库里没有这条记录。所以适配器里要写明错误归属让模型只能基于给定的错误原因回答不要自己发挥。3. 策略编排层把“能不能办、怎么办”交给规则而不是模型3.1 模型不能决定流程三个无法回避的理由工具调用解决了“模型能不能碰到业务系统”的问题但还有一个更关键的问题业务流程谁来定我的答案很明确规则来定不是模型来定。原因有三个。第一模型输出不稳定。同一句话今天可能提取出正确的意图明天换个表达方式就偏了。但保险流程是确定的——退保必须有身份校验理赔必须有材料清单这些不能跟着模型的随机性走。第二业务规则需要可解释。审核人员问“这个退保申请为什么走到人工审核”你不能回答说“因为模型觉得需要”要说清楚是哪条规则触发的。第三审计要求留痕。监管和合规会要求每个操作都能回溯规则引擎天然适合做这件事。所以我的设计是模型只负责两件事——识别用户意图、提取关键参数。意图和参数出来后交给策略引擎去匹配规则决定执行哪个流程动作。3.2 YAML化策略配置从试算到预审的完整例子策略用YAML配置而不是硬编码在代码里。这样做的好处是运营和风控人员也能看懂调整规则不用发版。下面是我实际用过的一个策略示例场景是“保费试算投保预审”。id: premium_trial name: 保费试算与投保预审 priority: 10 trigger: intents: - premium_trial - policy_precheck confidence_gte: 0.7 prechecks: - type: login_required fail_message: 我们这边需要先确认您的身份请先完成登录。 - type: field_required fields: [product_id, birthday] fail_message: 为了准确试算可以补充一下您的出生日期和意向的产品吗 steps: - name: 查询费率 call_tool: query_rate params: product_id: {{intent.product_id}} birthday: {{intent.birthday}} - name: 试算保费 call_tool: calc_premium params: base_rate: {{steps.query_rate.data.base_rate}} coverage: {{intent.coverage}} - name: 投保预审 call_tool: precheck_apply params: premium: {{steps.calc_premium.data.premium}} product_id: {{intent.product_id}} fallbacks: on_missing_param: action: ask_clarify on_limit_exceed: action: transfer_human on_tool_error: action: transfer_human这个配置表达的逻辑是只有当模型识别到“保费试算”或“投保预审”意图且置信度不低于0.7时这个策略才会触发。触发后先做两个前置校验——是否登录、参数是否齐全。校验通过后依次执行费率查询、保费计算、投保预审三个工具前面的工具结果会传给后面的工具作为参数。3.3 策略引擎执行流程校验、编排、回写与兜底策略引擎的执行流程可以拆成四段。第一段是意图匹配。模型输出的结构化意图会和所有策略的trigger进行比较命中且置信度达标才继续。第二段是前置校验。这里的校验包括登录状态、必填参数、产品白名单、用户限额。任何一项失败直接返回配置好的fail_message流程立即停止。第三段是步骤编排。每个步骤要么调用工具要么执行一个动作。步骤之间通过模板变量传递数据比如{{steps.query_rate.data.base_rate}}就是拿上一步工具返回的费率字段作为下一步入参。第四段是兜底与回写。任何一步失败都会进入fallbacks分支根据错误类型选择澄清、转人工或给出替代方案。这个设计里最容易被忽视的是回写。流程执行完后结果要写回会话上下文比如试算出来的保费金额、生成的预审单号后续用户再说“那就按这个方案投保吧”系统才能接得上。没有回写每次对话都是“失忆”的办理就办不完整。3.4 权限、限额与人工介入风控规则的落点风控规则在策略层有几个固定落点我建议做系统时就规划好不要后面再补。第一个是操作人身份校验。不是所有登录用户都能执行所有动作投保人只能操作自己的保单代理人有客户授权才能代办。这个校验放在策略的prechecks里而不是让模型去判断“用户是否有权限”。第二个是限额控制。比如单次试算的保额超过某个值不允许直接下单要转人工。这类规则用配置写死防止“聪明”的模型绕过去。第三个是敏感操作标记。退保、理赔、贷款这类动作我会在工具定义里标记sensitivetrue策略层一旦匹配到敏感操作自动增加一个“人工确认”步骤或者直接转到人工坐席。我把这些统称为“护栏”。护栏的目的是让系统在模型理解错误、用户在气头上、参数不完整等情况下依然能安全收场。保险行业一句不严谨的回复就可能引发投诉护栏越多上线越稳。4. 核心源代码实现主循环、工具注册与策略引擎4.1 主对话循环一个受约束的循环骨架下面这段代码是整个系统的核心循环。它的逻辑是把用户输入追加到会话历史让模型基于当前角色可见的工具列表生成回复如果模型决定调用工具就逐个执行并把结果追加回消息序列然后继续让模型推理循环次数有上限防止模型陷入无限调用。MAX_TOOL_STEPS 4 def run_conversation(session: Session, user_input: str) - str: session.messages.append({role: user, content: user_input}) for _ in range(MAX_TOOL_STEPS): available_tools registry.get_schemas(session.role) resp llm.chat( messagessession.messages, toolsavailable_tools, ) session.messages.append(resp.message) if resp.tool_calls: for call in resp.tool_calls: outcome registry.execute(call.name, call.arguments, session) audit.write(session.session_id, tool_call, call, outcome) session.messages.append({ role: tool, tool_call_id: call.id, content: outcome.safe_summary, }) continue intent extract_intent(resp.content) decision policy_engine.evaluate(session, intent) if decision.action run_flow: flow_result policy_engine.run_flow(decision.flow, session) audit.write(session.session_id, flow, decision.flow.id, flow_result) return flow_result.user_message if decision.action ask_clarify: return decision.message if decision.action transfer_human: return 您好这个需求需要转人工专员协助办理稍后客服会联系您。 return resp.content return 处理超时请重新描述需求。这里的MAX_TOOL_STEPS很关键。如果没有这个上限模型在遇到复杂任务时可能会反复调用工具甚至陷入循环既消耗token又拖延响应。4步在保险场景下通常够用查费率、算保费、预审、回写正好覆盖一个完整的最小办理流程。4.2 工具注册与按角色隔离的实现工具注册表的核心是两个功能按角色返回可见的工具schema列表执行工具前做权限校验。实现并不复杂关键是职责清晰。class ToolRegistry: def __init__(self): self._tools {} def register(self, tool: Tool): self._tools[tool.name] tool def get_schemas(self, role: str): return [ t.schema for t in self._tools.values() if role in t.allowed_roles and not t.disabled ] def execute(self, name: str, raw_args: dict, session: Session) - ToolResult: tool self._tools.get(name) if tool is None: return ToolResult(successFalse, errortool_not_found) if session.role not in tool.allowed_roles: audit.write(session.session_id, permission_denied, name, session.role) return ToolResult(successFalse, errorpermission_denied) try: valid_args tool.schema_validator(raw_args) except Exception: return ToolResult(successFalse, errorinvalid_params) result tool.handler(valid_args, session) summary tool.adapter(result) if tool.adapter else result return ToolResult(successTrue, dataresult, safe_summarysummary)注意我在执行前调用了schema_validator。这是为了防止模型传进来的参数不合法就直接打到业务系统。参数的合法性校验必须在工具执行前完成不要指望模型每次都严格遵守Schema。一个工具实例大概长这样tool Tool( namequery_rate, description( 查询某款保险产品的费率表。 仅当用户提到具体产品并要求计算保费、试算价格时使用。 如果用户没有指定产品名称或投保年龄则不要调用本工具。 ), parameters{ type: object, properties: { product_id: {type: string, description: 产品编号}, birthday: {type: string, format: date, description: 被保人生日YYYY-MM-DD}, }, required: [product_id, birthday], }, allowed_roles[customer, agent, service], sensitiveFalse, handlerquery_rate_handler, )4.3 策略解析与执行器从YAML到可执行动作策略引擎的代码设计上我分了evaluate和run_flow两步。evaluate负责找出命中的策略run_flow负责真正执行策略里的步骤。class PolicyEngine: def __init__(self, policy_dir: str): self.policies load_yaml_policies(policy_dir) def evaluate(self, session, intent): for p in sorted(self.policies, keylambda x: x.priority): if intent.name not in p.trigger_intents: continue if intent.confidence p.confidence_gte: continue return Decision(actionrun_flow, flowp) return Decision(actiondirect_reply) def run_flow(self, flow, session): ctx FlowContext(session.session_id) for step in flow.steps: if not self.run_prechecks(step.prechecks, ctx, session): return FlowResult(statusrejected, user_messagestep.prechecks.fail_message) if step.call_tool: params resolve_template(step.params, ctx) result registry.execute(step.call_tool, params, session) ctx.store(step.call_tool, result) if not result.success: fallback flow.fallbacks.get(on_tool_error) return self.apply_fallback(fallback, result, session) return ctx.finalize()这里的FlowContext负责保存每一步的执行结果并解析{{intent.product_id}}、{{steps.query_rate.data.base_rate}}这类模板变量。解析逻辑本质就是按路径取值但坑在于字段不存在时要快速失败不要抛一个难懂的KeyError而是转成策略里配置好的澄清信息。4.4 上下文存储与审计日志每个敏感动作都可追溯办理类系统里会话上下文不能只放在内存里。我用Redis按session_id维度存储流程中间态、后续步骤需要的数据都从Redis取。并发场景下不同会话的FlowContext天然隔离不会串数据。审计日志更是刚需。每一次工具调用、每一次策略决策、每一次人工介入都要记录。我惯用的审计字段有这几类会话ID、用户标识、角色、触发时间、事件类型tool_call/flow/transfer_human/denied、工具名或策略ID、请求参数、返回值摘要、耗时、是否成功。这些日志平时可能没人看但一旦出现客诉、监管抽查、线上问题回溯它就是唯一能还原现场的材料。我遇到过好几次问题定位全靠审计日志里的一条记录没有日志就只能靠猜。5. 实测中的意外情况五类问题与完整排查链路5.1 承诺收益被拦截之后还是漏了一次普通问答分支的兜底上线前我们就知道保险对话里“收益承诺”是红线。模型不能跟用户说“这款产品稳赚”“收益肯定很高”这类话术一旦出现就是合规事故。所以第一版系统在提示词里写了很多约束还加了一层敏感词过滤命中“保本”“稳赚”“一定赚”就直接拒绝回答。结果上线第二周还是漏了一条。用户问“这个年金险到时候能拿回来多少”模型回答“到期后领取总额非常可观是一笔可观的养老补充”。这句话没有命中敏感词但语义上已经偏向收益引导了。排查链路是这样的先看审计日志发现这条回复走了“无工具、无策略”的普通问答分支也就是说模型没有调用任何工具策略引擎也只返回了direct_reply。这个分支本身没有收益话术过滤只靠提示词在约束。修复方案是给普通问答分支也加一层收益语义检测。不光是词表匹配而是用一个小模型或者规则组合判断“是否包含对未来收益的确定性表述”。命中的话回复会被强制改写为“具体收益取决于合同条款建议参考产品说明书并咨询顾问”同时标记为风险会话转人工质检。这次之后我总结了一个教训任何“非办理”的普通问答分支也要纳入策略覆盖范围。不能因为没调用工具就觉得安全。5.2 工具返回null导致试算空白从日志定位到schema校验有一次用户反馈试算保费时页面转了几秒最后弹出一句“暂时无法试算请稍后再试”。从用户角度看就是一次失败但为了找到根因我们把整条链路拆了一遍。第一步翻审计日志定位到这次会话中有一次query_rate工具调用返回结果是successfalseerror_msg是“查无此产品费率”。第二步看请求参数发现product_id被填成了“unknown”。第三步往前追模型提取参数时没有识别到用户提到的产品名但依然调用了query_rate。问题出在工具描述和schema校验的配合上。工具描述里没有写“如果无法确定产品ID不要调用”而且schema_validator只校验了参数类型是字符串没有校验值是否在白名单里。模型拿了一个“unknown”字符串等于白调一次。修复有两处一是在query_rate描述里明确写“若用户未明确指定产品名称或产品ID无法映射不要调用”二是schema_validator改为校验product_id必须存在于产品白名单表里不在就直接返回invalid_params让主循环走澄清分支。这个缺陷如果没修用户每次含糊其辞都会触发一次无谓的工具调用。5.3 并发压测下状态串线本地缓存惹的祸这个坑比较隐蔽。压测工具模拟50个并发会话每个用户都在做保费试算。跑了一段时间后有同事在日志里发现用户A的试算结果里出现了用户B的出生日期。一开始怀疑是Redis key写错了排查半天发现所有key都是按session_id维度的没问题。后来查FlowContext的实现发现它内部有一个类级别的字典缓存初版为了“性能好”加的把一部分步骤结果存在了本地内存里而不是Redis。单机单线程时没问题并发一上来这个字典被多个请求共享数据就串了。修复方案很干脆——把FlowContext里所有状态全部改为Redis存储本地不保存任何中间状态。这个问题的根子在于“过早优化”Demo阶段用一个类变量缓存很方便但部署到多实例环境就会被放大。老实按session_id走外部存储虽然多几次IO但状态隔离是确定的。5.4 口语化的“那份保单”缺参澄清不硬匹配用户说“我之前买的那份保险是不是可以退了”这句话里没有保单号、没有产品名、没有投保人信息。如果模型硬提取参数大概率会填错或者编一个。第一版系统的做法是匹配到退保策略后发现缺少保单号就报一个“参数错误”。模型拿到错误后可能回复“请问您的保单号是多少”这个方向是对的但措辞僵硬而且用户如果记不住保单号对话就卡住了。后来把缺参处理统一改成了一家“澄清动作”。策略里配置on_missing_param时不再抛错误而是根据缺失字段选择澄清话术。比如缺少保单号时会说“为了准确帮您处理可以提供一下保单号吗如果没有的话发身份信息后四位也可以。”还可以配置多轮澄清上限两次都没有拿到有效参数就转人工。这个改动的思路是把模型提取参数的失败变成策略驱动的主动澄清流程而不是让模型自己临场发挥。5.5 预审流程白等几秒策略steps顺序的优先级教训还有一次问题不是模型造成的而是策略配置的顺序不合理。某个预审流程的steps是先调用保费试算工具再执行健康告知预审最后校验用户投保额度。用户在前两步都成功了最后一步发现额度超了提示“抱歉您的年度投保额度已用完暂无法继续”。用户白等了几秒钟实际上一开始就该被拦下来。问题出在策略的prechecks和steps划分上。投保额度校验属于前置条件应该放在prechecks里而不是流程跑完再做。修复很简单把额度校验从steps移到prechecks并调整了fail_message。事件本身不大但它让我意识到策略配置也需要一个“测试模式”能输入一条模拟对话输出完整的决策链——命中了哪个策略、通过了哪些prechecks、执行了哪些步骤、在哪一步失败。这个dry-run能力后来成了我们每次改策略之后的必经流程。6. 从代码到上线目录结构、部署要点与迭代方向6.1 模块划分目录结构与职责一个可以直接上手的项目结构我习惯按下面这样组织。它把工具、策略、引擎、适配、审计分成独立模块职责清晰团队协作时也不会互相踩。intelligent_processor/ ├── tools/ # 工具实现按业务域划分 │ ├── policy/ # 保单查询、费率试算、预审 │ ├── claim/ # 理赔预审、材料清单 │ └── common/ # 用户身份、风控、限额 ├── policies/ # YAML策略配置 ├── engine/ # 策略引擎 ├── registry/ # 工具注册表 ├── llm/ # 模型封装 ├── adapters/ # 返回值裁剪与适配 ├── audit/ # 审计日志 └── api/ # HTTP/WebSocket接口工具按业务域分目录策略全部收敛在policies目录下适配器和工具一一对应。这样新增一个业务能力时只需要在tools目录加实现、在policies加策略、在registry注册不用改动主循环。6.2 模型部署与成本脱敏、私有化与token控制模型选型上先考虑数据敏感度。如果会话里包含真实姓名、身份证号、保单金额我建议优先走私有化部署哪怕模型效果稍弱一点也不要把敏感数据送出去。如果只是做对外公开的产品咨询不涉及个人信息可以走API调用但要记得在入口做脱敏确保请求里不出现手机号、地址等关键字段。成本这块最大的变量是上下文长度。一个会话如果不断把长工具返回塞进历史几轮下来就可能上万token。我控制成本的三个手段一是工具返回结果裁剪只保留对话必要的摘要二是系统提示词精简业务规则尽量放到策略层而不是堆进prompt三是历史消息做滑动窗口只保留最近的五六轮更早的用摘要替代。如果你做的是内部系统按每个会话平均消耗token数、日活会话数、并发峰值可以粗略估算出日成本。以我这次的保守配置为例单会话平均消耗8000 token以内完全在可接受范围。6.3 不同角色看到不同工具集前端隐藏不够策略还要校验角色隔离是保险系统绕不开的需求。投保人、代理人、内勤坐席看到的工具集是不一样的。注册表里的get_schemas按角色过滤可以保证模型“看不到”不该用的工具。但这还不够执行链路里必须有二次校验。为什么因为模型虽然看不到某些工具但它在多轮对话中可能还记得之前某个schema的样子。而且角色可能切换比如用户从游客登录变成代理人登录工具集就变了。如果执行层不校验就有越权风险。所以在registry.execute里我无论从哪个入口进来都会检查session.role是否在tool.allowed_roles中。检查失败时审计日志会记录一条permission_denied这就是合规视角下的关键证据。6.4 迭代建议评测集、决策链可视化与多轮办理系统上线只是开始后面要持续迭代。我最先做的一件事是建一套评测集。从真实会话里抽了一千条脱敏对话标注好“期望意图”“期望工具调用”“期望策略结果”。每次改模型、改提示词、改策略都跑一遍评测集对比意图准确率、不该调用工具却调用的比例、流程成功率。没有评测集改动就是在盲飞。第二个方向是决策链可视化。策略引擎执行完一个流程后生成一张可读的决策链命中策略、前置校验结果、每一步工具调用、失败分支。运营和风控同事看到的是业务语言而不是代码逻辑。这个东西对排查问题特别有用。第三个方向是多轮办理。现在是一次会话内完成一个流程下一步要做的是跨轮流程推进。比如理赔申请分成“材料上传-信息核验-初审-人工复核”多个状态模型负责在每个状态收集必要信息状态机由策略引擎维护用户过几天再回来还能接着办。这个方向价值大但复杂度也上来了需要把策略的步骤定义升级成状态机定义。做这套系统下来我最大的体会是模型负责聪明规则负责稳。工具调用让模型有手有脚策略编排让规则看得见摸得着。如果你也在做类似的东西我建议先静下心把敏感操作一个个列出来用策略框死再研究怎么让模型更聪明。先有护栏再谈体验。这套方案跑通之后我们内部口径里“办成事”的比例有了明显提升但说实话让我更安心的不是那个比例而是每一个敏感动作都能在审计日志里对上号。这一步做好了后面加多少新场景都不慌。