diff --git a/docs/24-客服Agent阶段性总结与下阶段计划.md b/docs/24-客服Agent阶段性总结与下阶段计划.md index 7b89d5e..7ac5b0d 100644 --- a/docs/24-客服Agent阶段性总结与下阶段计划.md +++ b/docs/24-客服Agent阶段性总结与下阶段计划.md @@ -108,10 +108,10 @@ | 项 | 值 | |---|---| -| 分支 | `qyqy_develop_1`,领先 `origin` **13 个提交**(含本文,尚未推送) | -| 门禁 | `ruff` 干净 / `mypy` 113 文件无问题 / **497** unit+contract / 29 integration | +| 分支 | `qyqy_develop_1`(本表数字已按 2026-09-11 刷新,见文末"后续更新") | +| 门禁 | `ruff` 干净 / `mypy` **137** 文件无问题 / **664** unit+contract / **33** integration | | 知识库 | 636 块(faq / product / policy 三个集合) | -| 发布配置 | 版本 **181** active,5 条配置项 | +| 发布配置 | 版本 **201** active(`cs-prompt-d918ca507fc6`),**10** 条配置项:9 条 `agent_tools` + 1 条客服闲聊提示词 | | 新增测试 | 5 个文件:`test_knowledge_keyword_recall`、`test_knowledge_granularity`、`test_customer_service_search_query`、`test_customer_service_suitability`、`test_customer_service_topic_matrix` | | 新增工具 | `tools/chat_console.py` | @@ -152,14 +152,14 @@ a6c09fa 检索问句只在客户这一句说不清楚时才带上文 ### 后续功能(按依赖顺序) 6. **投顾 Agent**:画像(`fin_customer_profile` + `profile_snapshots`)、图投影(Neo4j)、适当性裁决**都已就绪**,可以开始。 -7. **风控 Agent**:需要先有规则引擎,尚未启动。 -8. **登录接口**:`docs/19` 里列为未解决项、当时有意推迟。前端要变成"能用"的产品级界面,这一步绕不过去。 +7. ~~**风控 Agent**:需要先有规则引擎,尚未启动。~~ **已完成**:规则引擎、扫描、研判、日报、通知和 15 个 REST 端点均已合并并通过门禁;代码评审与逐条处理结果见 `docs/25`。 +8. **登录接口**:**平台不实现**。`docs/05` §11 明确"JWT 签发、刷新、注销由统一身份认证模块负责,Agent 平台不重复实现",本平台只做校验(`app/core/security.py`)。前端要变"能用",正确做法是接统一认证模块,而不是在这里加一个登录端点。 ### 已知但暂不处理的缺口 - `projection_reconciliation_service` 的事件层 outbox(`MemorySyncOutbox`)**没有生产者**。 - `RUN_NOT_CANCELLABLE` 这个错误码被复用在了状态冲突场景(语义不贴切)。 -- `docs/05` §9.5 缺 `agent-intent-configs` 的 GET 说明;列表类接口普遍没有 `next_cursor`/`has_more`。 +- `docs/05` §9.5 缺 `agent-intent-configs` 的 GET 说明;~~列表类接口普遍没有 `next_cursor`/`has_more`~~ —— **风控侧已补齐**(信封对齐 §3.3、游标进 `meta`),客服侧仍待办。 - 客服热线仍是占位符 `400-XXX-XXXX`(`customer_service.py` 的 `HOTLINE`),真号码确定后应改为读配置项。 --- @@ -169,3 +169,44 @@ a6c09fa 检索问句只在客户这一句说不清楚时才带上文 如果你要在这套底座上加新 Agent,先读 `docs/19`(接入实操 + 11 个真实坑)和 `docs/23`(记忆分层与画像)。 **本阶段最值得抄走的一条经验**:新接一个能力之后,**别只看代码在不在,要跑一遍确认数据真的流过**。这个项目里"接口留好了、线没接上"出现过至少五次,而它们的表现都是**静默降级**——不报错,只是回答变得又慢又差,或者一直转人工。 + +--- + +## 七、后续更新(2026-09-11) + +本节只记录上面那份阶段总结**写完之后**发生的变化,原文保留为当时的事实。 + +### 7.1 本文第五节里那两件"要你先决策的事",都已落地 + +1. **客服闲聊提示词已落库**:发布版本 **201**(`cs-prompt-d918ca507fc6`)active,包含 + 9 条 `agent_tools` + 1 条客服闲聊提示词,共 10 条配置项。发布脚本 + `tools/publish_chitchat_prompt.py` 按当时的要求做了**继承**:读当前 active 版本的 + 配置项、映射字段、写入新版本,`config_release` 的整版本替换语义不会清空工具白名单。 + > 顺带记一条教训:`config_release` 是**整版本替换**,新增一项配置必须把老项带上, + > 否则激活的一瞬间老配置全部消失。这条已写进 `docs/05` 与评审报告。 +2. **风险测评记录**:`fin_risk_assessment` 的补数属于演示数据准备,不是代码问题; + 风控合并后适当性链路两侧(客服 `check_suitability` / 风控 RW-007)已按同一口径 + `C ≥ R` 复核通过。 + +### 7.2 风控模块合并后的处理(详见 `docs/25`) + +组员的风控模块(规则引擎 + 扫描 + 研判 + 日报 + 通知 + 15 个 REST 端点)已合并。 +评审报告 `docs/25` 逐条核实后的结果:**真缺陷已修,误报已更正,根因在别处的已改到别处**。 +重点几条: + +- 时区口径统一到**北京时间**(库内仍是 UTC naive,转换只在 `app/core/timeutil.py`); +- 列表信封对齐 `docs/05` §3.3、游标绑定用户与查询条件(§3.8); +- 6 个写接口接入平台幂等(§5.1),实现了 `ApiTransactionService.execute_in` 以免 + "内层提交外层事务"; +- 通知失败不再静默、扫描调度持久化、跨进程扫描锁、全量查询封顶并在响应里标注截断; +- `trigger_rule_codes` 补 JSON 多值索引(迁移 `20260911_risk_rule_index`), + `EXPLAIN` 由 `type=ALL` 变为 `type=range`; +- 第十五条豁免额度(C3→R4 ≤ 20%、C4→R5 ≤ 10%)落地到扫描与研判两侧。 + +### 7.3 三个仍然开着的口子 + +1. **客服热线仍是占位符** `400-XXX-XXXX`(`customer_service.py` 的 `HOTLINE`)。 +2. **`like(f"%{keyword}%")` 全表扫**:前后通配符在 B-tree 上无解,根治要全文索引 + + 中文分词组件,属部署依赖,本轮不做。 +3. **offset 游标 + 可变排序键**在真实并发下仍可能跳行/重复,根治要改 keyset 分页, + 属于分页协议级重做,未单方面改动。 diff --git a/docs/evidence/release-state.json b/docs/evidence/release-state.json new file mode 100644 index 0000000..9e3ee0b --- /dev/null +++ b/docs/evidence/release-state.json @@ -0,0 +1,51 @@ +{ + "recent_releases": [ + { + "id": 201, + "release_no": "cs-prompt-d918ca507fc6", + "title": "客服闲聊提示词", + "status": "active", + "created_at": "2026-09-11 05:25:44.185697", + "activated_at": "2026-09-11 05:25:44.742980" + }, + { + "id": 198, + "release_no": "probe-e68780d3f3", + "title": "探针验证 恢复", + "status": "superseded", + "created_at": "2026-09-11 05:12:33.626078", + "activated_at": "2026-09-11 05:12:34.229356" + }, + { + "id": 197, + "release_no": "probe-ee4036b9f8", + "title": "探针验证 分支4", + "status": "superseded", + "created_at": "2026-09-11 05:12:29.813587", + "activated_at": "2026-09-11 05:12:30.409705" + }, + { + "id": 196, + "release_no": "probe-aa84e8efd5", + "title": "探针验证 恢复", + "status": "superseded", + "created_at": "2026-09-11 05:12:11.881449", + "activated_at": "2026-09-11 05:12:12.528369" + }, + { + "id": 195, + "release_no": "probe-5eb538bf50", + "title": "探针验证 分支4", + "status": "superseded", + "created_at": "2026-09-11 05:12:08.861544", + "activated_at": "2026-09-11 05:12:09.497513" + } + ], + "active_items": [ + { + "namespace": "agent_tools", + "items": 9 + } + ], + "active_prompt_versions": 1 +} \ No newline at end of file diff --git a/tools/probe_release_state.py b/tools/probe_release_state.py new file mode 100644 index 0000000..290ea86 --- /dev/null +++ b/tools/probe_release_state.py @@ -0,0 +1,90 @@ +"""只读探查:当前 active 配置发布与配置项数量(用于同步 docs/24 的"当前状态"表)。 + +只做 SELECT,结果写 `docs/evidence/release-state.json`: + + python tools/probe_release_state.py +""" + +from __future__ import annotations + +import asyncio +import json +from pathlib import Path +from typing import Any + +from sqlalchemy import text + +from app.infrastructure.db import SessionFactory + +OUTPUT = Path("docs/evidence/release-state.json") + + +async def collect() -> dict[str, Any]: + report: dict[str, Any] = {} + async with SessionFactory() as session: + report["recent_releases"] = [ + dict(row) + for row in ( + ( + await session.execute( + text( + """ + SELECT id, release_no, title, status, created_at, activated_at + FROM config_release + ORDER BY id DESC + LIMIT 5 + """ + ) + ) + ) + .mappings() + .all() + ) + ] + report["active_items"] = [ + dict(row) + for row in ( + ( + await session.execute( + text( + """ + SELECT i.namespace, COUNT(*) AS items + FROM platform_config_item i + JOIN config_release r ON r.id = i.release_id + WHERE r.status = 'active' + GROUP BY i.namespace + ORDER BY i.namespace + """ + ) + ) + ) + .mappings() + .all() + ) + ] + report["active_prompt_versions"] = ( + await session.execute( + text( + """ + SELECT COUNT(*) FROM prompt_template_version p + JOIN config_release r ON r.id = p.release_id + WHERE r.status = 'active' + """ + ) + ) + ).scalar_one() + return report + + +async def main() -> None: + report = await collect() + OUTPUT.parent.mkdir(parents=True, exist_ok=True) + OUTPUT.write_text( + json.dumps(report, ensure_ascii=False, indent=2, default=str), + encoding="utf-8", + ) + print(f"wrote {OUTPUT}") + + +if __name__ == "__main__": + asyncio.run(main())