agentscope-ai / agentscope-ai/agentscope-java

[Feature] 支持缓存稳定的渐进式工具披露,避免 ToolGroup 变化导致 Prompt Cache 前缀整体失配

未关闭
#2,784 1 条评论 2 个 reaction 已指派 0 人 在 GitHub 查看
主要语言
Java
星标
5.6k
派生
1.3k
平均合并
4 天 12 小时
30 天内合并 PR
77

描述

## Summary

AgentScope 的 ToolGroup / reset_tools 支持按需激活工具,只向模型披露当前工具,从而降低工具 schema 数量。

但在 DashScope 显式 Context Cache 场景下,工具组一旦发生变化,发送给模型的完整 tools 数组也会变化。DashScope 会把完整工具定义计入 system 前缀,因此原有的 system
prompt、历史消息等缓存前缀都无法继续命中。

希望 AgentScope 提供一种可选的“Cache-Stable Tool Exposure”模式:既保留渐进披露和运行时权限约束,又避免动态工具变化破坏稳定的 system/history 缓存。

## Environment

- AgentScope Java:2.0.0
- Java:17
- Model provider:DashScope
- Model:qwen3.7-max
- Agent:ReActAgent
- Tool management:ToolGroup 渐进披露
- Context Cache:cache_control: {"type": "ephemeral"}
- Cache marker:
- 第一条 system 消息
- 每次请求最后一条文本消息

实际发送的 content-block 级 cache_control 已确认正确。

## Background

初始模型可见工具:

A = [
resolve,
reset_tools,
todo_write,
wait_async_results,
get_approval_progress
]

激活订单工具组后:

A+B = [
resolve,
reset_tools,
todo_write,
wait_async_results,
get_approval_progress,
query_orders,
get_order_detail,
count_orders
]

历史消息、系统提示词本身没有改变,只有工具面从 A 变成了 A+B。

但 DashScope 对缓存前缀的实际计算相当于:

旧缓存块:
K1 = system + 完整 tools[A]
K2 = system + 完整 tools[A] + history

新请求:
K1' = system + 完整 tools[A+B]
K2' = system + 完整 tools[A+B] + history

因为 tools[A] != tools[A+B],所以 K1 和 K2 都无法命中。

DashScope 官方文档也明确说明:工具定义会纳入 system 消息的缓存计算,并且工具定义不能单独设置缓存 marker。DashScope Context Cache
(https://help.aliyun.com/en/model-studio/context-cache)

AgentScope 官方 ToolGroup 设计则会在工具组激活后修改模型可见工具集合。AgentScope ToolGroup 文档 (https://java.agentscope.io/v2/en/docs/building-blocks/tool.html)

## Minimal Reproduction

### 第一次模型调用

{
"tools": ["base_tool_A"],
"messages": [
{
"role": "system",
"content": [{
"type": "text",
"text": "stable system prompt",
"cache_control": {"type": "ephemeral"}
}]
},
{
"role": "user",
"content": [{
"type": "text",
"text": "query an order",
"cache_control": {"type": "ephemeral"}
}]
}
]
}

模型调用 reset_tools,激活 order group。

### 第二次 ReAct 模型调用

{
"tools": [
"base_tool_A",
"query_orders_B",
"get_order_detail_B"
],
"messages": [
{"role": "system", "content": "...same system prompt..."},
{"role": "user", "content": "...same user message..."},
{"role": "assistant", "tool_calls": ["reset_tools"]},
{
"role": "tool",
"content": [{
"type": "text",
"text": "order group activated",
"cache_control": {"type": "ephemeral"}
}]
}
]
}

即使 system 和已有历史消息完全一致,第二次调用也无法命中第一次调用创建的缓存前缀。

### 对照实验

A → A :可以命中
A → A+B :缓存 miss
A+B → A+B :可以命中新的 A+B 缓存

## Actual Behavior

我们的真实调用采样:

模型调用:54 次
缓存命中:47 次
缓存未命中:7 次

其中:
- 1 次服务冷启动
- 6 次工具面变化

相同工具面重复调用可以命中;重复设置为相同工具列表也不会破坏缓存。

这说明 miss 与工具面变化具有明确对应关系。

## Expected Behavior

希望存在一种可选模式,使工具组激活时:

1. system prompt 和已有历史消息仍能复用旧缓存。
2. 只有新披露的工具信息和本轮新增消息需要处理。
3. 未激活工具在运行时仍然不可调用。
4. 保持当前动态 native tools 行为作为默认模式,避免兼容性变化。

## Capability Boundary

在当前 DashScope 协议下,以下三个条件无法同时满足:

1. 未激活工具完全不出现在顶层 tools 中。
2. 激活后把真实工具 schema 加入顶层 tools。
3. 工具变化后继续命中旧 system/history 前缀缓存。

因为 DashScope 会把完整 tools 计入 system 缓存前缀,而且不支持在工具数组内部设置独立缓存断点。

因此,仅调整 cache_control marker 位置无法解决该问题,需要改变工具暴露方式,或者由模型供应商提供分段缓存能力。

## Suggested Solution

### 方案一:稳定 Meta Tool + 动态发现/调用,推荐

模型始终只看到固定的少量 meta tools:

resolve_tool_groups
activate_tool_groups
invoke_tool
todo_write
wait_async_results

激活工具组时,不修改顶层 tools,而是通过 tool result 返回新工具的名称、说明和参数 schema:

{
"activatedGroups": ["order"],
"availableTools": [
{
"name": "query_orders",
"description": "Query orders by conditions",
"parameters": {
"type": "object",
"properties": {}
}
}
]
}

模型通过固定的 invoke_tool 调用真实工具:

{
"name": "invoke_tool",
"arguments": {
"toolName": "query_orders",
"toolArguments": {
"orderNo": "PO123"
}
}
}

AgentScope 运行时负责:

- 检查工具所属组是否已激活
- 按真实 ToolSchema 校验参数
- 调用对应的 AgentTool
- 返回结构化工具结果

这样顶层工具 schema 永远稳定:

cache prefix = system + 固定 meta tools + history

工具披露发生时,只在消息尾部追加新的工具说明,不会破坏前面的 system/history 缓存。

代价:

- 失去模型供应商对每个业务工具的原生参数约束
- 需要 AgentScope 在执行阶段完成二次 schema 校验
- 可能增加一次发现/激活调用

### 方案二:固定完整工具面 + 运行时权限控制

始终向模型发送全部工具 schema,ToolGroup 只控制工具是否允许执行。

优点:

- 实现简单
- Prompt Cache 最稳定
- 保留原生 function calling schema

缺点:

- 不再是真正的模型侧渐进披露
- 首轮工具 token 增加
- 工具较多时可能降低工具选择准确率

适合作为工具数量不多时的短期方案。

### 方案三:稳定主 Agent + 专业子 Agent

主 Agent 始终只暴露固定的领域代理工具:

order_agent
logistics_agent
approval_agent

每个专业 Agent 内部使用固定的业务工具集合。这样:

- 主 Agent 的 system/history/tool surface 始终稳定
- 各专业 Agent 分别形成自己的缓存前缀
- 保留业务工具的原生 schema

缺点是多一次 Agent 调用和上下文交接。AgentScope 已有 Agent as Tool (https://java.agentscope.io/en/task/agent-as-tool.html),但目前属于实验能力。

## Proposed API

可以考虑增加工具暴露策略:

public enum ToolExposureMode {
DYNAMIC_NATIVE_SCHEMA, // 当前行为
STABLE_SUPERSET_RUNTIME_GATED, // 全量稳定 schema,运行时控制权限
META_DISCOVERY_AND_DISPATCH // 固定 meta tool,动态发现和转发
}

示例:

ReActAgent.builder()
.toolkit(toolkit)
.toolExposureMode(ToolExposureMode.META_DISCOVERY_AND_DISPATCH)
.build();

也可以先提供更低层的扩展点,让应用自行实现:

- 模型可见工具 schema provider
- 运行时可执行工具 whitelist
- invokeTool(name, arguments) 统一分发入口
- 稳定的 ToolSchema 序列化与 fingerprint
- 模型调用前后的 tool-surface-change 事件

## Questions

1. AgentScope 当前是否已经存在“模型可见工具保持稳定,但执行权限随 ToolGroup 改变”的模式?
2. 是否有官方扩展点可以实现固定 invoke_tool,并复用 Toolkit 已有的参数校验和执行逻辑?
3. AgentScope 是否愿意增加 ToolExposureMode 一类的缓存友好模式?
4. 对 DashScope 场景,官方更推荐稳定 meta tool、完整工具面,还是 Agent-as-Tool?
5. 是否可以增加 tool-surface fingerprint 与 cached_tokens 观测,帮助识别工具 schema 顺序或结构变化造成的意外 miss?

## Acceptance Criteria

在 cache-stable 模式下:

- 激活 ToolGroup 前后,发送给模型的顶层 tools JSON 保持完全一致。
- 未激活工具不能被 invoke_tool 执行。
- 激活后能够根据真实 ToolSchema 校验参数并调用工具。
- 使用至少 1024 token 的稳定前缀,在 5 分钟内执行:

A → activate B → invoke B

激活后的模型调用仍能看到 cached_tokens > 0,且命中部分至少覆盖原 system prompt 和历史消息。

- 当前 DYNAMIC_NATIVE_SCHEMA 行为保持兼容。

贡献指南

打开贡献指南

评估

这个 Issue 还没有评估数据。

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。