diff --git a/internal/adapter/openai/handler_toolcall_format.go b/internal/adapter/openai/handler_toolcall_format.go index e69d048..0b10b3d 100644 --- a/internal/adapter/openai/handler_toolcall_format.go +++ b/internal/adapter/openai/handler_toolcall_format.go @@ -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\n \n tool_name\n {\"param\":\"value\"}\n \n\n\n【EXAMPLE】\nUser: Please check the weather in Beijing and Shanghai, and update my todo list.\nAssistant:\n\n \n get_weather\n {\"city\":\"Beijing\"}\n \n \n get_weather\n {\"city\":\"Shanghai\"}\n \n \n update_todo\n {\"todos\":[{\"content\":\"Buy milk\"},{\"content\":\"Write report\"}]}\n \n\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) `` MUST be strict JSON object text. Use double quotes for all JSON keys/strings.\n4) If calling multiple tools, emit multiple `` blocks under one `` 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_NAME_HERE + {"key":"value"} + + + +RULES: +1) Output ONLY the XML above when calling tools. Do NOT mix tool XML with regular text. +2) MUST contain a strict JSON object. All JSON keys and strings use double quotes. +3) Multiple tools → multiple blocks inside ONE 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. ... +Wrong 2 — code fence wrapping: + ` + "```xml\n ...\n ```" + ` +Wrong 3 — missing wrapper: + ` + ex1 + `{} + +✅ CORRECT EXAMPLES: + +Example A — Single tool: + + + ` + ex1 + ` + {"path":"src/main.go"} + + + +Example B — Two tools in parallel: + + + ` + ex1 + ` + {"path":"config.json"} + + + ` + ex2 + ` + {"path":"output.txt","content":"Hello world"} + + + +Example C — Tool with complex JSON parameters (newlines in values use \n): + + + ` + ex3 + ` + {"question":"Which approach do you prefer?","follow_up":[{"text":"Option A"},{"text":"Option B"}]} + + + +Remember: Output ONLY the ... XML block when calling tools.` +} func formatIncrementalStreamToolCallDeltas(deltas []toolCallDelta, ids map[int]string) []map[string]any { if len(deltas) == 0 { return nil diff --git a/internal/adapter/openai/prompt_build_test.go b/internal/adapter/openai/prompt_build_test.go index e820f52..bf8b285 100644 --- a/internal/adapter/openai/prompt_build_test.go +++ b/internal/adapter/openai/prompt_build_test.go @@ -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) } }