Repository navigation
Open
Description
报销额度查询会对不存在的员工返回"成功"结果(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_id、month 两个槽位,没有按姓名查工号的能力,于是追问工号 |
否 |
用户补充 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 的问题
- 工具契约里没有"未命中"语义。
expense.quota_query的output_schema只有employee_id/month/total_quota/used/remaining/currency/message,没有found/error_code之类字段。即使换成真实后端,只要它对未知工号返回默认额度,链路照样会把它当事实播报。对比本仓库backend/app/api/mock.py里的_order_miss/_product_miss,它们都显式带found: false和miss_reason。 - SOP 的响应节点只防"异常",不防"看似成功的空结果"。
node_response_result的指令把"工具成功"等价于"业务结果成立"。 - 用户提供的标识符被当作已验证事实。 姓名 → 工号的映射没有任何一环去查花名册或人员主数据;TL 在下一轮才用花名册否定了这条记录,说明系统里其实有可用的校验源,只是没接进 SOP。
- 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路由。演示行为不受版本控制,无法在仓库内修复或加测试。 - TL 侧 Trace 看不到成员的工具调用。
/api/enterprise/traces/{session_id}只返回 TL 会话的事件,tool_call_count=0,实际的harness_tool_completed落在成员会话里。排查时必须手动跳到team_tasks.session_id才能看到真正的工具入参出参。
建议修复
按优先级:
- 工具契约:
expense.quota_query(以及所有按工号查询的工具)的output_schema增加found: boolean与miss_reason;mock 端对未知工号返回found=false,与hr/balance_query、expense/submit对齐。 - SOP 响应节点:
node_response_result指令显式要求「若found=false或返回内容为默认值 / 未识别工号,必须告知用户未找到该员工,不得展示额度数字」。 - 身份校验前置:
node_collect_info拿到姓名或工号后,先查花名册 / 人员主数据确认存在,再进入工具调用;查不到就明确回复"未找到该员工",不追问工号。 - mock 收编进仓库:在
backend/app/api/mock.py补/expense/quota_query等路由,seed 里的 URL 改为可配置的本地地址,并加回归测试:未知工号必须返回found=false。 - Trace 可观测性:TL 会话的 trace 聚合下游成员会话的
harness_tool_completed事件,或至少在 TL 侧给出成员会话链接,避免"看 Trace 却看不到工具调用"。
复现步骤
- 在团队 TL 会话中发送:
帮我查询一下员工张三 2026 年 9 月的报销额度和剩余额度。 - 财务成员追问工号时,回复任意不存在的工号,例如
666。 - 观察 TL 回复给出 20,000 CNY 额度,且把用户输入的姓名与工号一并当作已确认员工信息输出。
- 直接请求
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.json:tools[10](expense.quota_query)、skills[14](skill_expense_quota_query)backend/app/api/mock.py:本地 mock,缺expense/quota_querybackend/app/api/teams.py:resume_team_task,用户补充的工号在此进入任务backend/app/api/traces.py:TL 侧 trace 不包含成员会话事件
Metadata
Metadata
Assignees
Labels
No labels
Activity