Files
2026-05-19 18:10:15 +08:00

11 KiB
Raw Permalink Blame History

素材专家检索 —— 内网接口对接说明

本文档说明 AI 智能写作中「素材收集专家」的检索机制、内网知识库接口的对接逻辑、 涉及的代码文件,以及常见错误的排查方式。


1. 检索来源切换

素材专家支持多种检索来源,通过后端配置文件统一切换,无需改动任何代码。

配置位置:offline-backend-20260512/backend/config.yaml

ai_writing:
  # 检索来源开关:internet=互联网检索(默认)/ intranet=内网知识库 / tavily / demo
  search_provider: internet
  # 以下两项仅在 search_provider=intranet 时生效
  intranet_search_url: http://10.106.57.180:7863/knowledge_base/search_docs_es
  intranet_knowledge_base: samples
search_provider 取值 含义
internet 互联网检索(DuckDuckGo),失败时回退到 demo 演示素材。当前默认值
intranet 内网知识库检索(调用 search_docs_es 接口)
tavily Tavily 互联网检索
demo 内置演示素材,仅用于本地调试

内网接口联通后,把 search_provider 改为 intranet 即可切换。 配置文件支持热加载(get_app_config() 按 mtime 自动重载),改完一般无需重启后端。


1.5 检索关键词数量

素材专家的检索词数量由用户在写作表单中指定(「检索关键词数量」输入框, 范围 1~10,默认 6)。数量越多素材越全面,但检索与排序更耗时。

  • 该参数随 POST /api/ai-writing/start 的 keyword_count 字段传入,写入 AIWritingState.keyword_count。
  • researcher_node 用它决定让 LLM 生成多少个检索词、并发检索多少个方向 (EXTRACT_KEYWORDS_USER prompt 按该数量格式化,search_keywords = keywords[:keyword_count])。
  • 非法值(缺省 / 越界 / 非数字)由前后端双重兜底:后端 _resolve_keyword_count() 规整到 [1, 10],越界回退默认 6。
  • 补充搜索(re_search)时用户额外添加的关键词不会被该上限截断。

1.6 素材总量上限

素材收集专家最终保留的素材总条数由后端配置文件统一控制,并按检索词数均分到每个词。

配置位置:offline-backend-20260512/backend/config.yaml 的 ai_writing 段:

ai_writing:
  # 素材总量上限,范围 1~100,非法值回退 20
  max_materials: 20

均分规则 —— 每个检索词的检索条数 = ⌈ max_materials / 实际检索词数 ⌉(向上取整):

max_materials 检索词数 每词检索条数
20 1 20
20 2 10
20 3 7(共 21,合并后截断到 20)
20 5 4
  • 各检索词按上表条数检索 → 合并去重 → 最终统一截断到 max_materials。
  • 向上取整是为了保证去重后素材数仍能接近上限;去重后实际条数可能略少于上限(不同词命中同一篇),属正常。
  • 与 1.5 的关系:keyword_count 决定搜几个词,max_materials 决定总共要多少条,两者相除得到每词条数。
  • 涉及代码:researcher.py 的 _resolve_max_materials()(读配置 + 规整)与 _per_keyword_limit()(均分计算)。

config.yaml 支持热加载,改完一般无需重启后端。


2. 内网接口规格

  • 地址:http://10.106.57.180:7863/knowledge_base/search_docs_es

  • 方法:POST

  • 请求体:

    {
      "query": "检索关键词",
      "knowledge_base_name": "samples",
      "ts": "yes"
    }
    
  • 响应体:一个数组,每个元素结构如下:

    [
      {
        "page_content": "正文片段",
        "metadata": {
          "m_title":  "标题",
          "content":  "内容",
          "recUuid":  "原文id",
          "m_publish":"发布时间",
          "source1":  "来源"
        }
      }
    ]
    

    我们需要的字段都在 metadata 里。

字段映射

后端把内网响应映射为统一的 SearchResult 结构:

内网响应字段 SearchResult 字段 说明
metadata.m_title title 标题,缺省为「(无标题)」
metadata.content content 内容,截断到 500 字
metadata.source1 source 来源,缺省为「内网知识库」
metadata.m_publish published_at 发布时间
metadata.recUuid recUuid 原文 id,用于前端拼接「查看原文」链接
— url 固定为空,原文地址由前端依据 recUuid 拼接
— relevance_score 接口不返回分值,按返回顺序赋递减分以保留 ES 排序
— source_type 固定为 "intranet"

3. 对接逻辑与涉及文件

后端

文件 作用
packages/harness/deerflow/agents/ai_writing/search/intranet_stub.py 内网检索适配器 IntranetSearchProvider。发起 POST 请求、解析响应、映射为 SearchResult
packages/harness/deerflow/agents/ai_writing/search/__init__.py get_search_provider() 工厂函数,依据 search_provider 配置返回对应 provider
packages/harness/deerflow/agents/ai_writing/search/base.py SearchResult 数据结构 + BaseSearchProvider 抽象基类
packages/harness/deerflow/agents/ai_writing/nodes/researcher.py 素材收集专家节点,调用 provider 检索并把结果合并、排序为素材包
app/gateway/routers/ai_writing.py _ai_writing_config() 读取 config.yaml 的 ai_writing 段,经 _make_config() 注入运行时
config.yaml ai_writing 段,检索来源开关与内网接口地址

调用链:

ai_writing.py(_make_config 注入 ai_writing_config)
  → researcher_node
    → get_search_provider(ai_writing_config)   # 依据 search_provider 选择
      → IntranetSearchProvider.search()         # POST search_docs_es
        → SearchResult[]                        # 字段映射

前端

文件 作用
src/core/config/index.ts INTRANET_SOURCE_URL_BASE 原文详情页地址前缀 + buildIntranetSourceUrl() 构造函数
src/open-canvas/components/ai-writing/MaterialItem.tsx 素材卡片,recUuid 非空时显示「查看原文」按钮
src/open-canvas/components/ai-writing/CompletedInterventionCard.tsx 历史回显的素材卡,「原文」链接同样基于 recUuid
src/open-canvas/api/ai-writing.ts normalizeMaterial() 把后端 snake_case 字段转为前端 camelCase(含 recUuid)

「查看原文」链接拼接规则:

INTRANET_SOURCE_URL_BASE + recUuid
= https://chlib.b.uat.4.cn/knowlibrary/#/knowledge/mid_detail?recUuid={recUuid}
  • 仅当素材的 recUuid 非空时才显示「查看原文」按钮。
  • 点击直接 window.open(...) 新标签页打开,不做站内跳转。

4. 错误处理与排查

4.1 内网检索返回空 / 调不通

现象:切到 intranet 后,素材专家时间线出现琥珀色告警「ES 波动导致检索异常」, 下方显示真实报错原因;若完全无结果,再提示「未检索到任何素材」。

IntranetSearchProvider.search() 对所有异常(连接失败、超时、非 2xx、JSON 解析失败) 都做了兜底:捕获后记录 warning 日志、把原因收集到 provider.errors,并返回空列表, 不会让整个写作流程崩溃。researcher_node 在检索结束后取出 provider.errors, 以非致命的 notice 事件推送到前端时间线(标题「ES 波动导致检索异常」+ 真实原因), 接口报错不影响写作流程继续。

容错相关代码:

  • 检索异常收集:intranet_stub.py / duckduckgo_search.py 的 errors 列表
  • 告警事件发出:researcher.py 的 _notice() 与 progress_events
  • 模型调用异常:researcher.py(检索词/排序)、writer_outline.py / writer_draft.py / editor.py 均会发出「模型错误」事件(致命错误用 error,可兜底的用 notice)
  • 前端展示:TimelineStep.tsx 渲染 notice(琥珀)/ error(红色)的标题 + 真实原因

排查步骤:

  1. 确认网络可达:在后端所在机器执行 curl -X POST http://10.106.57.180:7863/knowledge_base/search_docs_es -H "Content-Type: application/json" -d "{\"query\":\"测试\",\"knowledge_base_name\":\"samples\",\"ts\":\"yes\"}"
  2. 查看后端日志:搜索关键字 intranet search failed,会打印具体异常(连接超时 / DNS / 401 等)。
  3. 确认配置:config.yaml 的 intranet_search_url、intranet_knowledge_base 是否正确。

空结果时前端已有兜底:素材确认卡会弹出「未检索到任何素材,是否在无素材的情况下继续写作」, 用户可以选择「重新搜索」或「无数据继续写作」。

4.2 字段缺失 / 响应结构变化

search() 对每个字段都用 or 兜底(如 m_title 缺失时回退为「(无标题)」), 单条数据字段缺失不会导致整批失败。

若内网接口改为「数组外包一层 data/results」的结构,适配器已兼容; 若改成其它结构,需修改 intranet_stub.py 中解析 items 的逻辑。

4.3 「查看原文」按钮不显示

原因:该素材的 recUuid 为空。这是预期行为 —— 只有内网知识库素材才带 recUuid, 互联网检索(internet/tavily/demo)的素材没有 recUuid,不显示「查看原文」。

如需让互联网素材也能跳转,需另行调整 MaterialItem.tsx 的显示条件。

4.4 原文链接打不开 / 地址不对

「查看原文」地址前缀写在 src/core/config/index.ts 的 INTRANET_SOURCE_URL_BASE。 若内网原文详情页地址有变,只改这一个常量即可。

4.5 检索到的素材全是无关内容

现象:素材卡片内容与写作意图完全不沾边。

排查:检查后端日志里素材专家实际用的检索词。常见原因是模型把关键词提取 提示词模板里的占位符(如「关键词1」「...」)当成真关键词去检索了。

researcher.py 的 _clean_keywords() 已对此做防御:剔除 关键词\d* / keyword\d* / ... 等占位符,全部被滤掉时回退为写作意图本身。若仍出现,检查 researcher_prompts.py 的 EXTRACT_KEYWORDS 提示词是否又引入了易被误抄的示例占位符。


5. 联调上线步骤

  1. 确认后端机器能访问 10.106.57.180:7863。
  2. 把 config.yaml 的 ai_writing.search_provider 改为 intranet。
  3. 后端配置热加载生效(或重启后端)。
  4. 进入 AI 智能写作页面,发起一次写作,观察素材收集专家是否返回内网素材。
  5. 校验素材卡片的标题/来源/时间显示正确,「查看原文」能跳到内网详情页。
  6. 如检索为空,按 4.1 排查。