From 77262f8c3aa90d7fa101a964fc4939ff03715183 Mon Sep 17 00:00:00 2001 From: qing <1776177662@qq.com> Date: Mon, 17 Aug 2026 23:42:04 +0800 Subject: [PATCH] fix(aibox): inject available model profile names into create_subagent (M2) The LLM had no way to know which profile names exist in the models config section; a hallucinated name fell through to bare-name semantics (default endpoint + bogus model) and failed at request time with API 400. SubAgentTool now builds the model hint dynamically from config via _model_names_hint(): description and the model parameter's JSON Schema description both list the profile names, with the default profile (api.default_model) marked. Empty or malformed models sections keep the original wording untouched (zero breakage); exceptions degrade to no hint. README: tool table note, test count 293, new v3.4 changelog entry summarizing the H1/H2/H3/M2 profile-hardening batch. Tests: new TestM2SubAgentModelHint (5 cases); full suite 293 passed. --- aibox/README.md | 12 +++++-- aibox/aibox.py | 31 ++++++++++++++++-- aibox/tests/test_models_profile.py | 51 ++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 5 deletions(-) diff --git a/aibox/README.md b/aibox/README.md index 447cab8..b3bea3d 100644 --- a/aibox/README.md +++ b/aibox/README.md @@ -92,7 +92,7 @@ aibox/ │ ├── prompt_tools.py # B 批:UpdateIdentity/UpdateSoul/SaveMemoryAndEndConversation │ ├── proxy_tools.py # B 批:SetProxy/GetProxy │ └── misc_tools.py # B 批:AskUserTool -├── tests/ # 单元测试套件(264 项全过) +├── tests/ # 单元测试套件(293 项全过) │ ├── conftest.py # sys.path 配置 + websockets 桩 │ ├── test_basic_tools.py # A 批工具 43 项 │ ├── test_b_tools.py # B 批工具 58 项 @@ -190,7 +190,7 @@ py -m pytest aibox/tests -q | `memory` | 保存、搜索(支持分页)、删除记忆 | | `memo` | 创建、查看、完成、搜索任务 | | `express_emotion` | 表达情绪并记录当前情绪状态(anger/joy/sadness/surprise),可指定 `persona` 从 `~/.aibox/skills/` 加载人格技能(如 angry/coquettish/tsundere)派生携带该人格的情绪子代理,保证人格独立、不污染主对话上下文 | -| `create_subagent` | 派生子代理:独立上下文/角色设定,仅拥有执行面工具(无记忆/人格/派生/提问能力),可选独立模型,用于长任务分解、上下文隔离、独立角色专注处理 | +| `create_subagent` | 派生子代理:独立上下文/角色设定,仅拥有执行面工具(无记忆/人格/派生/提问能力),可选独立模型(工具描述动态列出 `models` 节可用档案名,防 LLM 编造名称),用于长任务分解、上下文隔离、独立角色专注处理 | | `search_skills` | 搜索扩展技能(支持 Markdown 和可执行技能),查到后派生子代理执行 | | `reload_skills` | 热重载扩展技能 | | `create_skill` | 创建新技能(AI 自主编写 Python 脚本,实际写入经子代理完成) | @@ -884,6 +884,14 @@ elif command == "/mycommand": ## 📄 更新日志 +### v3.4 +- ✅ **多模型档案加固(H1/H2/H3/M2 四项修复)**: + - **H1(凭据隔离)**:档案端点环境变量取不到时直接报错,**绝不回退读 `~/.cert/deepseek.key`**(该文件为 DeepSeek 专属凭据,跨厂商复用属于泄漏);key 文件回退仅对无档案的旧 `api` 扁平节生效 + - **H2(schema 校验前移)**:档案缺 `api_url`/`model` 或类型错误时,解析期即抛出指明档案名与缺失字段的 `ValueError`——不再把档案名当模型名发出、不再延迟到 `requests InvalidURL` 或 API 400 + - **H3(--model 服务模式生效)**:`--model` 升级为进程级覆盖,WebSocket 服务模式(主对话/每个会话实例/定时任务实例)与 `--once` 均生效,此前服务模式静默忽略 + - **M2(防模型名幻觉)**:`create_subagent` 工具描述与 JSON Schema 动态携带 `models` 节可用档案名列表(默认档案标注),LLM 无从编造名称;`models` 为空时描述保持原样(零破坏) +- ✅ **测试**:293 项全过(H1 隔离 4 项 + H2 校验 6 项 + H3 传导 6 项 + M2 提示 5 项新增) + ### v3.3 - ✅ **安全审计与修复**(两轮审计报告见 [docs/reports/aibox_audit_20260817.md](../docs/reports/aibox_audit_20260817.md)): - **Batch E(命令执行保护链修复)**:`CommandLineTool.execute_streaming` 纯委托注入的 executor(删除绕过 1MB/超时/中断保护的 150 行重复 Popen 实现);`SubprocessCommandExecutor` 读取循环线程化(后台 reader 线程 + 0.1s 超时轮询,静默命令超时不再失效);两处 `Popen` 改 `stdin=DEVNULL`(等待输入的命令不再永久挂起工具线程);write_file 指引 heredoc 定界符唯一化;registry 流式转发循环加 3600s 总超时兜底。PoC 回归 5/5 + 全量单测通过 diff --git a/aibox/aibox.py b/aibox/aibox.py index beaa22a..fca022c 100644 --- a/aibox/aibox.py +++ b/aibox/aibox.py @@ -748,19 +748,44 @@ class SubAgentTool(Tool): def get_name(self) -> str: return "create_subagent" + def _model_names_hint(self) -> str: + """M2:从配置读取可用模型档案名,注入工具描述供 LLM 选择。 + + 此前 LLM 不知道有哪些合法档案名,幻觉出的名字走裸名语义 + (默认端点 + 错误模型名),运行时 API 400。models 节为空时 + 返回空串,工具描述保持原样(零破坏)。 + """ + try: + models = self.config.get("models") or {} + if not isinstance(models, dict) or not models: + return "" + default_name = self.config.get("api.default_model") + names = [f"{n}(默认)" if n == default_name else str(n) for n in models] + return f"可用模型档案:{', '.join(names)}" + except Exception: + return "" + def get_description(self) -> str: - return """派生子代理执行任务。子代理拥有全部工具能力和独立的上下文窗口,不会污染主对话上下文。 + hint = self._model_names_hint() + model_line = "- model:子代理使用的模型(可选,默认与主代理一致)" + if hint: + model_line += f"。{hint},请从中选择,不要编造其他名称" + return f"""派生子代理执行任务。子代理拥有全部工具能力和独立的上下文窗口,不会污染主对话上下文。 适用场景:需要多步骤操作的任务(如文件处理、命令执行、信息检索)、需要独立上下文的复杂任务。 子代理可进行多轮工具调用,任务完成后由子代理基于完整执行过程生成最终报告(结论/关键过程/产物清单/未解决问题)返回。 参数说明: - role:子代理的角色设定(作为系统提示词),如"你是一位资深代码审查专家" - task:要交给子代理执行的具体任务 -- model:子代理使用的模型(可选,默认与主代理一致) +{model_line} - temperature:采样温度(可选,默认使用主配置) - max_tokens:最大输出 token 数(可选,默认 2048) - max_turns:最大对话轮次(可选,默认 10,防止无限循环)""" def get_parameters(self) -> Dict: + model_desc = "子代理使用的模型名称,默认与主代理一致" + hint = self._model_names_hint() + if hint: + model_desc += f"。{hint},请从中选择" return { "type": "object", "properties": { @@ -774,7 +799,7 @@ class SubAgentTool(Tool): }, "model": { "type": "string", - "description": "子代理使用的模型名称,默认与主代理一致" + "description": model_desc }, "temperature": { "type": "number", diff --git a/aibox/tests/test_models_profile.py b/aibox/tests/test_models_profile.py index edb4b31..9b499e8 100644 --- a/aibox/tests/test_models_profile.py +++ b/aibox/tests/test_models_profile.py @@ -388,6 +388,57 @@ class TestH3ModelPlumbing: assert args.model == "kimi" +# ==================== M2:create_subagent 工具注入档案名列表 ==================== + +class TestM2SubAgentModelHint: + """M2:LLM 此前不知道合法档案名,幻觉名走裸名语义(默认端点+错误模型)→ API 400。 + + 验证 SubAgentTool 的 description 与 model 参数 description 动态携带 + models 节档案名列表(默认档案标注),models 为空时保持原样(零破坏)。 + """ + + def _tool_with(self, models, default_model="deepseek"): + from aibox import SubAgentTool + tool = SubAgentTool(logger=None) + tool.config = FakeConfig({ + "models": models or {}, + "api": {"default_model": default_model}, + }) + return tool + + def test_description_lists_profiles(self): + """工具描述包含全部档案名""" + tool = self._tool_with(MODELS) + desc = tool.get_description() + assert "kimi" in desc and "local" in desc and "deepseek" in desc + assert "可用模型档案" in desc + + def test_default_profile_marked(self): + """默认档案带(默认)标注""" + tool = self._tool_with(MODELS, default_model="kimi") + desc = tool.get_description() + assert "kimi(默认)" in desc + + def test_param_schema_lists_profiles(self): + """JSON Schema 的 model 参数 description 同样携带档案名""" + tool = self._tool_with(MODELS) + model_desc = tool.get_parameters()["properties"]["model"]["description"] + assert "kimi" in model_desc and "deepseek" in model_desc + + def test_empty_models_keeps_original(self): + """models 为空 → 描述保持原文案,不含提示语(零破坏)""" + tool = self._tool_with({}) + desc = tool.get_description() + assert "可用模型档案" not in desc + model_desc = tool.get_parameters()["properties"]["model"]["description"] + assert model_desc == "子代理使用的模型名称,默认与主代理一致" + + def test_non_dict_models_tolerated(self): + """models 节类型异常(如字符串)→ 容错返回空提示""" + tool = self._tool_with("not-a-dict") + assert "可用模型档案" not in tool.get_description() + + # ==================== DeepSeekChat 档案级 reasoning 读取 ==================== class TestChatReasoningFlag: