refactor: optimize tool-calling prompt instructions and examples for improved model adherence

This commit is contained in:
CJACK
2026-03-29 15:18:43 +08:00
parent 3ab9d44f60
commit f041ebab93
2 changed files with 90 additions and 6 deletions

View File

@@ -53,7 +53,7 @@ func injectToolPrompt(messages []map[string]any, tools []any, policy util.ToolCh
if len(toolSchemas) == 0 {
return messages, names
}
toolPrompt := "You have access to these tools:\n\n" + strings.Join(toolSchemas, "\n\n") + "\n\nWhen you need to use tools, output ONLY XML using this canonical format:\n<tool_calls>\n <tool_call>\n <tool_name>tool_name</tool_name>\n <parameters>{\"param\":\"value\"}</parameters>\n </tool_call>\n</tool_calls>\n\n【EXAMPLE】\nUser: Please check the weather in Beijing and Shanghai, and update my todo list.\nAssistant:\n<tool_calls>\n <tool_call>\n <tool_name>get_weather</tool_name>\n <parameters>{\"city\":\"Beijing\"}</parameters>\n </tool_call>\n <tool_call>\n <tool_name>get_weather</tool_name>\n <parameters>{\"city\":\"Shanghai\"}</parameters>\n </tool_call>\n <tool_call>\n <tool_name>update_todo</tool_name>\n <parameters>{\"todos\":[{\"content\":\"Buy milk\"},{\"content\":\"Write report\"}]}</parameters>\n </tool_call>\n</tool_calls>\n\nIMPORTANT:\n1) If calling tools, output ONLY XML in the canonical format above. Do NOT include any extra text.\n2) Do NOT wrap tool-call XML in markdown/code fences (for example, do not use triple backticks).\n3) `<parameters>` MUST be strict JSON object text. Use double quotes for all JSON keys/strings.\n4) If calling multiple tools, emit multiple `<tool_call>` blocks under one `<tool_calls>` root.\n5) After receiving a tool result, you MUST use it to produce the final answer.\n6) Only call another tool when the previous result is missing required data or returned an error."
toolPrompt := "You have access to these tools:\n\n" + strings.Join(toolSchemas, "\n\n") + "\n\n" + buildToolCallInstructions(names)
if policy.Mode == util.ToolChoiceRequired {
toolPrompt += "\n7) For this response, you MUST call at least one tool from the allowed list."
}
@@ -73,6 +73,90 @@ func injectToolPrompt(messages []map[string]any, tools []any, policy util.ToolCh
return messages, names
}
// buildToolCallInstructions generates the tool-calling instruction block
// with attention-optimized structure: rules → negative examples → positive
// examples (using real tool names) → anchor. This ordering exploits the
// transformer's recency bias: the last tokens before generation starts
// carry the strongest influence on the model's first output tokens.
func buildToolCallInstructions(toolNames []string) string {
// Pick real tool names for examples; fall back to generic names.
ex1 := "read_file"
ex2 := "write_to_file"
ex3 := "ask_followup_question"
used := map[string]bool{}
for _, n := range toolNames {
switch {
case !used["ex1"] && (n == "read_file" || n == "list_files" || n == "search_files"):
ex1 = n
used["ex1"] = true
case !used["ex2"] && (n == "write_to_file" || n == "apply_diff" || n == "execute_command"):
ex2 = n
used["ex2"] = true
case !used["ex3"] && (n == "ask_followup_question" || n == "attempt_completion" || n == "update_todo_list"):
ex3 = n
used["ex3"] = true
}
}
return `TOOL CALL FORMAT — FOLLOW EXACTLY:
When calling tools, emit ONLY raw XML. No text before, no text after, no markdown fences.
<tool_calls>
<tool_call>
<tool_name>TOOL_NAME_HERE</tool_name>
<parameters>{"key":"value"}</parameters>
</tool_call>
</tool_calls>
RULES:
1) Output ONLY the XML above when calling tools. Do NOT mix tool XML with regular text.
2) <parameters> MUST contain a strict JSON object. All JSON keys and strings use double quotes.
3) Multiple tools → multiple <tool_call> blocks inside ONE <tool_calls> root.
4) Do NOT wrap the XML in markdown code fences (no triple backticks).
5) After receiving a tool result, use it directly. Only call another tool if the result is insufficient.
6) If you want to say something AND call a tool, output text first, then the XML block on its own.
❌ WRONG — Do NOT do these:
Wrong 1 — mixed text and XML:
I'll read the file for you. <tool_calls><tool_call>...
Wrong 2 — code fence wrapping:
` + "```xml\n <tool_calls>...\n ```" + `
Wrong 3 — missing <tool_calls> wrapper:
<tool_call><tool_name>` + ex1 + `</tool_name><parameters>{}</parameters></tool_call>
✅ CORRECT EXAMPLES:
Example A — Single tool:
<tool_calls>
<tool_call>
<tool_name>` + ex1 + `</tool_name>
<parameters>{"path":"src/main.go"}</parameters>
</tool_call>
</tool_calls>
Example B — Two tools in parallel:
<tool_calls>
<tool_call>
<tool_name>` + ex1 + `</tool_name>
<parameters>{"path":"config.json"}</parameters>
</tool_call>
<tool_call>
<tool_name>` + ex2 + `</tool_name>
<parameters>{"path":"output.txt","content":"Hello world"}</parameters>
</tool_call>
</tool_calls>
Example C — Tool with complex JSON parameters (newlines in values use \n):
<tool_calls>
<tool_call>
<tool_name>` + ex3 + `</tool_name>
<parameters>{"question":"Which approach do you prefer?","follow_up":[{"text":"Option A"},{"text":"Option B"}]}</parameters>
</tool_call>
</tool_calls>
Remember: Output ONLY the <tool_calls>...</tool_calls> XML block when calling tools.`
}
func formatIncrementalStreamToolCallDeltas(deltas []toolCallDelta, ids map[int]string) []map[string]any {
if len(deltas) == 0 {
return nil

View File

@@ -71,19 +71,19 @@ func TestBuildOpenAIFinalPrompt_VercelPreparePathKeepsFinalAnswerInstruction(t *
}
finalPrompt, _ := buildOpenAIFinalPrompt(messages, tools, "")
if !strings.Contains(finalPrompt, "After receiving a tool result, you MUST use it to produce the final answer.") {
if !strings.Contains(finalPrompt, "After receiving a tool result, use it directly.") {
t.Fatalf("vercel prepare finalPrompt missing final-answer instruction: %q", finalPrompt)
}
if !strings.Contains(finalPrompt, "Only call another tool when the previous result is missing required data or returned an error.") {
if !strings.Contains(finalPrompt, "Only call another tool if the result is insufficient.") {
t.Fatalf("vercel prepare finalPrompt missing retry guard instruction: %q", finalPrompt)
}
if !strings.Contains(finalPrompt, "output ONLY XML using this canonical format") {
if !strings.Contains(finalPrompt, "TOOL CALL FORMAT") {
t.Fatalf("vercel prepare finalPrompt missing xml format instruction: %q", finalPrompt)
}
if !strings.Contains(finalPrompt, "Do NOT wrap tool-call XML in markdown/code fences") {
if !strings.Contains(finalPrompt, "Do NOT wrap the XML in markdown code fences") {
t.Fatalf("vercel prepare finalPrompt missing no-fence xml instruction: %q", finalPrompt)
}
if strings.Contains(finalPrompt, "```xml") || strings.Contains(finalPrompt, "```json") {
if strings.Contains(finalPrompt, "```json") {
t.Fatalf("vercel prepare finalPrompt should not require fenced tool calls: %q", finalPrompt)
}
}