Files
group_fqcd_jr/app/service/knowledge_tool.py
T
张胜宇 9675df8453 chore(sync): zsy_developcc 全量同步至 qyqy_develop(W26 口径)
- 分支内容对齐 qyqy_develop b6ec3aa,树完全一致(同步后 git diff 为空)
- 覆盖本轮全部交付:客服 Agent 重构(安全路由 / 五出口 / 记忆与画像 / RAG 全链路)
  + 开发文档 62 份编号体系(D1.1 v1.17 索引)
  + 新增 D2.10-客服Agent端到端答辩文档-2026-09-21.html
- 基线:e239eb7(2026-09-17 品牌口径统一快照),本提交为其直接后继
2026-09-21 21:26:30 +08:00

59 lines
2.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""知识检索工具:注册给 Agent 的**只读**公共工具。
为什么把知识检索做成工具,而不是让 Agent 直接持有检索服务:走 `ToolExecutor` 就同时
得到四件由基座保证的事——工具白名单(发布配置可收窄、缺配置即失败关闭)、权限校验、
调用审计(`agent.tool_executed`)、超时保护。Agent 拿到的 `source_references` 也由基座
统一附加,业务代码不能伪造来源引用。
工具只读是硬约束(`ToolRegistry.register` 会拒绝 `read_only=False`),本工具确实只查库。
"""
from typing import Any
from app.core.contracts import RequestContext
from app.core.knowledge_contracts import KnowledgeSearchInput
from app.core.knowledge_tier import tiers_for_roles
async def knowledge_search_tool(
arguments: KnowledgeSearchInput, context: RequestContext
) -> dict[str, Any]:
"""检索三个知识集合,返回命中原文与来源信息。
检索链路(向量化 / Milvus)任一环节失败都**不抛异常**,而是以 `degraded=True` 返回:
客服 Agent 据此走「引导客户致电人工客服」,而不是把基础设施故障暴露成客户可见的错误。
"""
# 档位由**鉴权结果**决定,不由查询内容决定:访客令牌的上下文只有 `roles=("visitor",)`,
# 客户登录后是 `("customer",)`。映射表在 `knowledge_contracts.TIERS_BY_SUBJECT`,
# 业务代码**不手写档位字面量** —— 这样「忘记过滤」与「传错档位」在签名层就不可能发生
# (`tiers` 是必填参数,没有默认值可依赖)。
tiers = tiers_for_roles(context.roles)
# 延迟导入:bootstrap 会导入本模块完成工具注册,模块级导入会形成循环依赖。
from app.service.agent.bootstrap import get_knowledge_search_service
outcome = await get_knowledge_search_service().search(
arguments.query,
tiers=tiers,
collections=(arguments.collection,) if arguments.collection else None,
top_k=arguments.top_k,
)
return {
"degraded": outcome.degraded,
"reason": outcome.reason,
"hits": [
{
"doc_id": hit.doc_id,
"title": hit.title,
"content": hit.content,
"score": round(hit.score, 4),
"source_file": hit.source_file,
"doc_no": hit.doc_no,
"visibility": hit.visibility,
"family_id": hit.family_id,
"param_class": hit.param_class,
"intent": hit.intent,
}
for hit in outcome.hits
],
}