Skip to content

报销额度查询对不存在的员工返回"成功"结果:expense.quota_query 缺少存在性校验,导致 TL 播报虚构员工额度 #257

Description

@BlueX888

报销额度查询会对不存在的员工返回"成功"结果(expense.quota_query 无存在性校验)

现象

在团队 TL 对话中询问「员工张三 2026 年 9 月的报销额度」,财务成员追问工号,用户随手补充 666。系统随后回复:

员工张三(工号 666)2026年9月的报销额度为 20,000.00 CNY,当前已用 0.00 CNY,剩余额度 20,000.00 CNY,额度充足。

「张三」和工号 666 在花名册、知识库、任何人员主数据中都不存在。用户随后反问,TL 才承认查不到该员工。

复现会话:session_80ac377dd3c64f23(TL)→ 成员会话 session_861fcb66894243a2,团队任务 task_ebe76a65f8ef4ed1

根因(按数据流逐层排查)

发生了什么 是否校验员工存在
TL turn_planner 创建 TaskFrame,slot_hints={employee_name: 张三},指派财务成员
SOP skill_expense_quota_query / node_collect_info 只抽取 employee_idmonth 两个槽位,没有按姓名查工号的能力,于是追问工号
用户补充 666/tasks/{id}/resume 原文写入 review_json.comment,唤醒成员 rework 否(也不应该)
node_call_quota_query employee_id=666 直接调用 HTTP 工具 expense.quota_query
外部 mock http://58.57.119.30:52008/api/mock/expense/quota_query 对任意 employee_id 都返回 HTTP 200 + total_quota=20000 否 ← 根因
node_response_result 指令是"根据工具返回的 total_quota/used/remaining 展示",只在"工具返回异常或数据缺失"时说明情况;工具既没异常也没缺失,于是如实转述
TL 汇总 原文转发成员报告

直接对该 mock 端点探测:

employee_id=666            → total_quota=20000, remaining=20000, "额度充足,请安心使用"
employee_id=999999         → total_quota=20000, remaining=20000
employee_id=DOES_NOT_EXIST → total_quota=20000, remaining=20000, "额度充足,请安心使用"
employee_id=""             → employee_id 被改写为 "UNKNOWN",仍然 total_quota=20000

同一 mock 主机上的兄弟端点行为并不一致:hr/balance_query 对不存在工号会返回「未找到工号为 … 的员工记录」,expense/submit 会直接 rejected: 员工工号不存在。只有 expense/quota_query 对任何输入都"查得到"。

模型没有虚构数字:harness_tool_completed 事件里的 result.data 与最终回复完全一致。「张三」这个名字来自用户输入,被 SOP 存进 employee_name 槽位后与工具结果拼在一起输出,从而形成"张三(工号 666)额度 20000"这个看起来像查到真人的答案。

为什么这是系统性问题,而不只是 mock 的问题

  1. 工具契约里没有"未命中"语义。 expense.quota_queryoutput_schema 只有 employee_id/month/total_quota/used/remaining/currency/message,没有 found/error_code 之类字段。即使换成真实后端,只要它对未知工号返回默认额度,链路照样会把它当事实播报。对比本仓库 backend/app/api/mock.py 里的 _order_miss/_product_miss,它们都显式带 found: falsemiss_reason
  2. SOP 的响应节点只防"异常",不防"看似成功的空结果"。 node_response_result 的指令把"工具成功"等价于"业务结果成立"。
  3. 用户提供的标识符被当作已验证事实。 姓名 → 工号的映射没有任何一环去查花名册或人员主数据;TL 在下一轮才用花名册否定了这条记录,说明系统里其实有可用的校验源,只是没接进 SOP。
  4. mock 地址硬编码为外部 IP,且仓库内没有对应实现。 backend/app/db/seed_fixtures/staffdeck_admin_gallery_seed.json 里 11 个工具全部指向 58.57.119.30:52008,本地 app/api/mock.py 没有 /expense/quota_query 路由。演示行为不受版本控制,无法在仓库内修复或加测试。
  5. TL 侧 Trace 看不到成员的工具调用。 /api/enterprise/traces/{session_id} 只返回 TL 会话的事件,tool_call_count=0,实际的 harness_tool_completed 落在成员会话里。排查时必须手动跳到 team_tasks.session_id 才能看到真正的工具入参出参。

建议修复

按优先级:

  1. 工具契约expense.quota_query(以及所有按工号查询的工具)的 output_schema 增加 found: booleanmiss_reason;mock 端对未知工号返回 found=false,与 hr/balance_queryexpense/submit 对齐。
  2. SOP 响应节点node_response_result 指令显式要求「若 found=false 或返回内容为默认值 / 未识别工号,必须告知用户未找到该员工,不得展示额度数字」。
  3. 身份校验前置node_collect_info 拿到姓名或工号后,先查花名册 / 人员主数据确认存在,再进入工具调用;查不到就明确回复"未找到该员工",不追问工号。
  4. mock 收编进仓库:在 backend/app/api/mock.py/expense/quota_query 等路由,seed 里的 URL 改为可配置的本地地址,并加回归测试:未知工号必须返回 found=false
  5. Trace 可观测性:TL 会话的 trace 聚合下游成员会话的 harness_tool_completed 事件,或至少在 TL 侧给出成员会话链接,避免"看 Trace 却看不到工具调用"。

复现步骤

  1. 在团队 TL 会话中发送:帮我查询一下员工张三 2026 年 9 月的报销额度和剩余额度。
  2. 财务成员追问工号时,回复任意不存在的工号,例如 666
  3. 观察 TL 回复给出 20,000 CNY 额度,且把用户输入的姓名与工号一并当作已确认员工信息输出。
  4. 直接请求 POST http://58.57.119.30:52008/api/mock/expense/quota_query{"employee_id":"DOES_NOT_EXIST","month":"2026-09"},得到同样的 20,000 额度。

相关位置

  • backend/app/db/seed_fixtures/staffdeck_admin_gallery_seed.jsontools[10]expense.quota_query)、skills[14]skill_expense_quota_query
  • backend/app/api/mock.py:本地 mock,缺 expense/quota_query
  • backend/app/api/teams.pyresume_team_task,用户补充的工号在此进入任务
  • backend/app/api/traces.py:TL 侧 trace 不包含成员会话事件

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions