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.
This commit is contained in:
1 parent
84a3f6dd5e
commit
77262f8c3a
3 files changed
+89
-5
No files matched your search
+10
-2
@@ -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 + 全量单测通过
|
||||
|
||||
+28
-3
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user