️ perf: optimize tool system prompt — remove duplicate APIs, simplify XML tags (#13041)

* 💄 style: remove platform-specific Spotlight reference from searchLocalFiles

Replace "using Spotlight (macOS) or native search" with "using native search"
since the actual search implementation is platform-dependent and the LLM
doesn't need to know the specific backend.

Fixes LOBE-5778

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ️ perf: remove duplicate API descriptions from tool system prompt

API identifiers and descriptions are already in the tools schema passed
via the API tools parameter. Repeating them in the system prompt wastes
tokens. Now only tools with systemRole (usage instructions) are injected.

Also rename XML tags: plugins→tools, collection→tool,
collection.instructions→tool.instructions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 💄 style: inject tool description when no systemRole instead of skipping

Tools without systemRole now show their description as <tool> children.
Tools with systemRole use <tool.instructions> wrapper as before.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 💄 style: always emit <tool> tag, fallback to "no description"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* update tools

* fix

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arvin Xu
2026-03-17 02:01:05 +08:00
committed by GitHub
parent 9bdc3b0474
commit 44e4f6e4b0
9 changed files with 94 additions and 85 deletions

View File

@@ -34,7 +34,7 @@ You have access to a set of tools to interact with the user's local file system:
9. **killCommand**: Terminate a running background shell command by its ID.
**Search & Find:**
10. **searchLocalFiles**: Searches for files based on keywords and other criteria using Spotlight (macOS) or native search. Use this tool to find files if the user is unsure about the exact path.
10. **searchLocalFiles**: Searches for files based on keywords and other criteria using native search. Use this tool to find files if the user is unsure about the exact path.
11. **grepContent**: Search for content within files using regex patterns. Supports various output modes, filtering, and context lines.
12. **globLocalFiles**: Find files matching glob patterns (e.g., "**/*.js", "*.{ts,tsx}").
</core_capabilities>

View File

@@ -23,7 +23,7 @@ You have access to a set of tools to interact with the user's local file system:
9. **killCommand**: Terminate a running background shell command by its ID.
**Search & Find:**
10. **searchLocalFiles**: Searches for files based on keywords and other criteria using Spotlight (macOS) or native search. Use this tool to find files if the user is unsure about the exact path.
10. **searchLocalFiles**: Searches for files based on keywords and other criteria using native search. Use this tool to find files if the user is unsure about the exact path.
11. **grepContent**: Search for content within files using regex patterns. Supports various output modes, filtering, and context lines.
12. **globLocalFiles**: Find files matching glob patterns (e.g., "**/*.js", "*.{ts,tsx}").
</core_capabilities>

View File

@@ -369,7 +369,7 @@ describe('MessagesEngine', () => {
// Should inject tool system role when manifests are provided
const systemMessage = result.messages.find((msg) => msg.role === 'system');
expect(systemMessage).toBeDefined();
expect(systemMessage!.content).toContain('tool1');
expect(systemMessage!.content).toContain('Tool 1');
});
it('should skip tool system role provider when no tools', async () => {

View File

@@ -107,6 +107,7 @@ export class ToolSystemRoleProvider extends BaseProvider {
name: this.toolNameResolver.generate(manifest.identifier, api.name, manifest.type),
}),
),
description: manifest.meta?.description,
identifier: manifest.identifier,
name: manifest.meta?.title || manifest.identifier,
systemRole: manifest.systemRole,

View File

@@ -16,11 +16,12 @@ const createMockManifests = (identifiers: string[]): LobeToolManifest[] =>
identifier: id,
api: [{ name: 'action', description: `${id} action`, parameters: {} }],
meta: { title: id },
systemRole: `Instructions for ${id}`,
type: 'default' as const,
}));
describe('ToolSystemRoleProvider', () => {
it('should inject tool system role when manifests are provided and FC is supported', async () => {
it('should inject tool system role when manifests with systemRole are provided and FC is supported', async () => {
const mockIsCanUseFC = () => true;
const manifests = createMockManifests(['calculator', 'weather']);
@@ -36,11 +37,12 @@ describe('ToolSystemRoleProvider', () => {
const ctx = createContext(messages);
const result = await provider.process(ctx);
// Should have system message with tool system role
// Should have system message with tool instructions
const systemMessage = result.messages.find((msg) => msg.role === 'system');
expect(systemMessage).toBeDefined();
expect(systemMessage!.content).toContain('calculator');
expect(systemMessage!.content).toContain('weather');
expect(systemMessage!.content).toContain('<tool name="calculator">');
expect(systemMessage!.content).toContain('<tool name="weather">');
expect(systemMessage!.content).toContain('Instructions for calculator');
// Should update metadata
expect(result.metadata.toolSystemRole).toBeDefined();
@@ -48,6 +50,34 @@ describe('ToolSystemRoleProvider', () => {
expect(result.metadata.toolSystemRole!.supportsFunctionCall).toBe(true);
});
it('should skip injection when manifests have apis but no systemRole', async () => {
const mockIsCanUseFC = () => true;
const manifests: LobeToolManifest[] = [
{
identifier: 'no-instructions',
api: [{ name: 'action', description: 'some action', parameters: {} }],
meta: { title: 'No Instructions' },
type: 'default',
},
];
const provider = new ToolSystemRoleProvider({
manifests,
model: 'gpt-4',
provider: 'openai',
isCanUseFC: mockIsCanUseFC,
});
const messages = [{ id: 'u1', role: 'user', content: 'Do something' }];
const ctx = createContext(messages);
const result = await provider.process(ctx);
// Has apis → still injects tool info even without systemRole
const systemMessage = result.messages.find((msg) => msg.role === 'system');
expect(systemMessage).toBeDefined();
expect(systemMessage!.content).toContain('No Instructions');
});
it('should merge tool system role with existing system message', async () => {
const mockIsCanUseFC = () => true;
const manifests = createMockManifests(['calculator']);

View File

@@ -18,12 +18,9 @@ describe('pluginPrompts', () => {
},
];
const expected = `<plugins description="The plugins you can use below">
<collection name="tool1">
<api identifier="api1">API 1</api>
</collection>
</plugins>`;
const expected = `<tools description="The tools you can use below">
<tool name="tool1">no description</tool>
</tools>`;
expect(pluginPrompts({ tools })).toBe(expected);
});
@@ -31,10 +28,6 @@ describe('pluginPrompts', () => {
it('should generate plugin prompts without tools', () => {
const tools: Tool[] = [];
const expected = `<plugins description="The plugins you can use below">
</plugins>`;
expect(pluginPrompts({ tools })).toBe(expected);
expect(pluginPrompts({ tools })).toBe('');
});
});

View File

@@ -2,11 +2,12 @@ import type { Tool } from './tools';
import { toolsPrompts } from './tools';
export const pluginPrompts = ({ tools }: { tools: Tool[] }) => {
const prompt = `<plugins description="The plugins you can use below">
${toolsPrompts(tools)}
</plugins>`;
const content = toolsPrompts(tools);
if (!content) return '';
return prompt.trim();
return `<tools description="The tools you can use below">
${content}
</tools>`;
};
export { type API, apiPrompt, type Tool, toolPrompt, toolsPrompts } from './tools';

View File

@@ -4,104 +4,83 @@ import type { Tool } from './tools';
import { apiPrompt, toolPrompt, toolsPrompts } from './tools';
describe('Prompt Generation Utils', () => {
// 测试 apiPrompt 函数
describe('apiPrompt', () => {
it('should generate correct api prompt', () => {
const api = {
name: 'testApi',
desc: 'Test API Description',
};
const api = { name: 'testApi', desc: 'Test API Description' };
expect(apiPrompt(api)).toBe(`<api identifier="testApi">Test API Description</api>`);
});
});
// 测试 toolPrompt 函数
describe('toolPrompt', () => {
it('should generate tool prompt with system role', () => {
it('should use tool.instructions when systemRole is present', () => {
const tool: Tool = {
name: 'testTool',
identifier: 'test-id',
systemRole: 'Test System Role',
apis: [
{
name: 'api1',
desc: 'API 1 Description',
},
],
description: 'Short desc',
systemRole: 'Detailed instructions',
apis: [{ name: 'api1', desc: 'API 1' }],
};
const expected = `<collection name="testTool">
<collection.instructions>Test System Role</collection.instructions>
<api identifier="api1">API 1 Description</api>
</collection>`;
expect(toolPrompt(tool)).toBe(expected);
expect(toolPrompt(tool)).toBe(`<tool name="testTool">
<tool.instructions>Detailed instructions</tool.instructions>
</tool>`);
});
it('should generate tool prompt without system role', () => {
it('should use description as children when no systemRole', () => {
const tool: Tool = {
name: 'testTool',
identifier: 'test-id',
apis: [
{
name: 'api1',
desc: 'API 1 Description',
},
],
description: 'A useful tool for testing',
apis: [{ name: 'api1', desc: 'API 1' }],
};
const expected = `<collection name="testTool">
expect(toolPrompt(tool)).toBe(`<tool name="testTool">A useful tool for testing</tool>`);
});
<api identifier="api1">API 1 Description</api>
</collection>`;
it('should fallback to "no description" when no systemRole and no description', () => {
const tool: Tool = {
name: 'testTool',
identifier: 'test-id',
apis: [{ name: 'api1', desc: 'API 1' }],
};
expect(toolPrompt(tool)).toBe(expected);
expect(toolPrompt(tool)).toBe('<tool name="testTool">no description</tool>');
});
});
// 测试 toolsPrompts 函数
describe('toolsPrompts', () => {
it('should generate tools prompts with multiple tools', () => {
it('should include tools with systemRole and description', () => {
const tools: Tool[] = [
{
name: 'tool1',
identifier: 'id1',
apis: [
{
name: 'api1',
desc: 'API 1',
},
],
systemRole: 'Instructions for tool1',
apis: [{ name: 'api1', desc: 'API 1' }],
},
{
name: 'tool2',
identifier: 'id2',
apis: [
{
name: 'api2',
desc: 'API 2',
},
],
description: 'Tool 2 description',
apis: [{ name: 'api2', desc: 'API 2' }],
},
{
name: 'tool3',
identifier: 'id3',
apis: [{ name: 'api3', desc: 'API 3' }],
},
];
const expected = `<collection name="tool1">
<api identifier="api1">API 1</api>
</collection>
<collection name="tool2">
<api identifier="api2">API 2</api>
</collection>`;
const expected = `<tool name="tool1">
<tool.instructions>Instructions for tool1</tool.instructions>
</tool>
<tool name="tool2">Tool 2 description</tool>
<tool name="tool3">no description</tool>`;
expect(toolsPrompts(tools)).toBe(expected);
});
it('should generate tools prompts with empty tools array', () => {
const tools: Tool[] = [];
expect(toolsPrompts(tools)).toBe('');
it('should return empty for empty tools array', () => {
expect(toolsPrompts([])).toBe('');
});
});
});

View File

@@ -4,6 +4,7 @@ export interface API {
}
export interface Tool {
apis: API[];
description?: string;
identifier: string;
name?: string;
systemRole?: string;
@@ -11,11 +12,15 @@ export interface Tool {
export const apiPrompt = (api: API) => `<api identifier="${api.name}">${api.desc}</api>`;
export const toolPrompt = (tool: Tool) =>
`<collection name="${tool.name}">
${tool.systemRole ? `<collection.instructions>${tool.systemRole}</collection.instructions>` : ''}
${tool.apis.map((api) => apiPrompt(api)).join('\n')}
</collection>`;
export const toolPrompt = (tool: Tool) => {
if (tool.systemRole) {
return `<tool name="${tool.name}">
<tool.instructions>${tool.systemRole}</tool.instructions>
</tool>`;
}
return `<tool name="${tool.name}">${tool.description || 'no description'}</tool>`;
};
export const toolsPrompts = (tools: Tool[]) => {
const hasTools = tools.length > 0;