fix(ai写作): 修复内网技能检不出东西(技能目录路径 + 模型 + 兜底 + 开关)

对比主智能体与 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) <noreply@anthropic.com>
This commit is contained in:
fengxiangdebaoziandClaude Opus 4.8 committed 2026-06-24 23:52:32 +08:00
1 parent d8b43e7579
commit c0305c9269
3 files changed
+71 -2

No files matched your search

@@ -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-<stage>/ 的 SOUL.md + config(admin 可在智能体
# 管理页编辑人设/模型),false / 缺失 = 走旧的硬编码 prompt 路径。
@@ -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:
@@ -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:保留旧素材 + 追加新检索结果