From c0305c92695d19266800565abc00617cbeb67d0e Mon Sep 17 00:00:00 2001 From: fengxiangdebaozi <10845145+fengxiangdebaozi@user.noreply.gitee.com> Date: Wed, 24 Jun 2026 23:52:32 +0800 Subject: [PATCH] =?UTF-8?q?fix(ai=E5=86=99=E4=BD=9C):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=86=85=E7=BD=91=E6=8A=80=E8=83=BD=E6=A3=80=E4=B8=8D=E5=87=BA?= =?UTF-8?q?=E4=B8=9C=E8=A5=BF=EF=BC=88=E6=8A=80=E8=83=BD=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=20+=20=E6=A8=A1=E5=9E=8B=20+=20=E5=85=9C?= =?UTF-8?q?=E5=BA=95=20+=20=E5=BC=80=E5=85=B3=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对比主智能体与 AI 写作两份调用日志定位到根因:真实 agent 路径只注入了 SKILL.md 内容、没告诉模型技能在容器内的目录,模型用相对路径跑脚本 (python3 scripts/search)→ No such file。主智能体则 cd 进技能绝对目录再跑。 - _skill_agent:注入 skill.get_container_path() 的绝对目录,要求先 cd 再执行 脚本/用绝对路径(对齐主智能体 skill_view 拿 location 后的行为)。 - researcher:跑技能的 agent 改用用户选定的写作主模型(model_name_fallback=None), 不再用常配成外网的 keyword_model,避免技能 agent 连不上自己的模型。 - researcher:所有技能都 0 条时,最终兜底直连内网 ES 接口(IntranetSearchProvider), 受 ai_writing.intranet_search_url 控制。 - config.yaml:开 researcher_skill_agent=true(内网技能必须走真实运行时), 新增 intranet_search_url / intranet_knowledge_base 兜底配置项。 Co-Authored-By: Claude Opus 4.8 (1M context) --- offline-backend-20260512/backend/config.yaml | 10 +++++ .../agents/ai_writing/nodes/_skill_agent.py | 20 ++++++++- .../agents/ai_writing/nodes/researcher.py | 43 ++++++++++++++++++- 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/offline-backend-20260512/backend/config.yaml b/offline-backend-20260512/backend/config.yaml index ee7b9d07..1fd52073 100644 --- a/offline-backend-20260512/backend/config.yaml +++ b/offline-backend-20260512/backend/config.yaml @@ -425,6 +425,16 @@ ai_writing: keyword_model: deepseek-chat # 检索词生成专用模型,留空回退写作主模型 rank_model: deepseek-chat # 素材排序专用模型,留空回退写作主模型 max_materials: 20 # 合并去重后的素材总量上限,范围 [1, 100],默认 20 + # 用「真实智能体运行时」跑配置技能(read SKILL.md + 全套工具 + 沙箱),让技能的真实 + # 检索逻辑(调内网接口/MCP/读文件)真正执行,而不是塞进通用 web/ES 检索 provider。 + # 内网技能必须开 true,否则技能会走通用 provider、连不上内网而报 ConnectError。 + # 仅对非「知识库检索(knowledge-base-search)」的配置技能生效;失败回退直连检索。 + researcher_skill_agent: true + # 最终兜底:当所有技能都没检索到素材时,直连内网知识库 ES 接口兜底检索(避免素材全空)。 + # 配上你内网 ES 接口地址即启用;留空则不启用该兜底。 + # 接口契约:POST {url} 体 {"query","knowledge_base_name","ts":"yes"} → [{"page_content","metadata":{m_title,content,recUuid,m_publish,source1}}] + intranet_search_url: "" # 例如 http://10.106.57.180:7863/knowledge_base/search_docs_es + intranet_knowledge_base: "samples" # 兜底检索用的知识库名 # 四阶段「内置功能型智能体」开关(按阶段灰度)。true = 该阶段节点改为加载 # .deer-flow/agents/ai-writing-/ 的 SOUL.md + config(admin 可在智能体 # 管理页编辑人设/模型),false / 缺失 = 走旧的硬编码 prompt 路径。 diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/_skill_agent.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/_skill_agent.py index b89c9acd..b0951756 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/_skill_agent.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/_skill_agent.py @@ -114,7 +114,25 @@ async def run_skill_via_real_agent( logger.warning("skill real-agent[%s]: build tools failed: %s", skill_name, e) return [], f"工具初始化失败: {e}" - system_prompt = (soul + "\n\n" if soul else "") + _MATERIAL_OUTPUT_CONTRACT + # 技能在容器内的目录:注入 SKILL.md 时模型只有内容、不知道脚本在哪,会用相对路径 + # (python3 scripts/xxx.py)在错误的 CWD 下跑 → No such file。把绝对目录告诉它,让它 + # 先 cd 进去再执行(与「别的智能体」里 skill_view/read_file 拿到 location 后的行为一致)。 + try: + base_path = getattr(getattr(app_config, "skills", None), "container_path", None) or "/mnt/skills" + except Exception: # noqa: BLE001 + base_path = "/mnt/skills" + try: + skill_dir = skill.get_container_path(base_path) + except Exception: # noqa: BLE001 + skill_dir = "" + skill_loc_hint = ( + f"\n\n【技能目录(重要)】技能「{skill_name}」在容器内的目录是:{skill_dir}\n" + f"- 运行该技能自带的脚本时,**务必先 `cd {skill_dir}` 再执行**" + f"(例如 `cd {skill_dir} && python3 scripts/xxx.py ...`),或对脚本用绝对路径;\n" + "- **不要用相对路径**(如 `python3 scripts/xxx.py`),否则当前目录不对会报 No such file。" + ) if skill_dir else "" + + system_prompt = (soul + "\n\n" if soul else "") + _MATERIAL_OUTPUT_CONTRACT + skill_loc_hint task_lines = [f"写作意图:{intent or '(无)'}"] if user_query: diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/researcher.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/researcher.py index 59797efa..7f4afede 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/researcher.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/researcher.py @@ -725,7 +725,11 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict user_query=effective_user_query if is_user_query_search else "", config=config, state=state, - model_name_fallback=keyword_model, + # 跑技能的 agent 用**用户选定的写作主模型**(state.model_name),不要用 + # keyword_model:那是「检索词生成快模型」,常配成外网模型(如 deepseek-chat + # → api.deepseek.com),内网跑技能 agent 会连不上模型本身。传 None 让 + # resolve_agent_model_name 回退到 state.model_name(用户在表单选的、确定可达)。 + model_name_fallback=None, max_results=per_skill, timeout_s=_SKILL_SEARCH_TIMEOUT_SECONDS, emit_query=_emit_q, @@ -810,6 +814,43 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict # 兜底已拿到素材即止,不再无谓尝试后续技能 break + # ── 最终兜底:技能(含兜底技能)都没检索到 → 直连内网知识库接口(ES)兜一把 ── + # 用户诉求:技能因网络/配置连不上而 0 条时,至少用内网 ES 接口直检,避免素材全空。 + # 需在 config.yaml 的 ai_writing 段配 intranet_search_url 才启用;接口契约见 + # IntranetSearchProvider(POST {query,knowledge_base_name,ts} → [{page_content,metadata}])。 + if not new_results: + intranet_url = (ai_writing_config.get("intranet_search_url") or "").strip() + if intranet_url: + from ..search.intranet_stub import IntranetSearchProvider + + _emit_skill_step({ + "type": "fallback", + "message": "技能均未检索到素材,改为直连内网知识库接口兜底检索…", + }) + intranet = IntranetSearchProvider( + base_url=intranet_url, + knowledge_base_name=ai_writing_config.get("intranet_knowledge_base", "samples"), + ) + fb_keywords = keywords[:keyword_count] or [intent] + fb_gathered = await asyncio.gather( + *[intranet.search(kw, max_results=per_skill) for kw in fb_keywords], + return_exceptions=True, + ) + fb_added = 0 + for fb_results in fb_gathered: + if isinstance(fb_results, Exception): + search_errors.append(f"{type(fb_results).__name__}: {fb_results}") + continue + fb_added += _collect(fb_results) + search_errors.extend(intranet.errors) + # 让下方 notice 拿到内网接口的原始响应体(解析失败时便于定位) + provider = intranet + _emit_skill_step({ + "type": "skill_result", + "message": f"内网知识库接口兜底检索到 {fb_added} 条新素材", + "matched_count": fb_added, + }) + # 合并去重 # - 首次检索 / 普通 re_search(无输入,用户只想重出):旧素材丢弃,覆盖式替换 # - 素材不足求助 supplement_search:保留旧素材 + 追加新检索结果