feat(toolcall): prioritize XML for model output and parsing

This commit is contained in:
CJACK.
2026-03-29 10:53:38 +08:00
parent 6e8f3185d5
commit 958f4e39b5
7 changed files with 90 additions and 54 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 this JSON object format:\n{\"tool_calls\": [{\"name\": \"tool_name\", \"input\": {\"param\": \"value\"}}]}\n\n【EXAMPLE】\nUser: Please check the weather in Beijing and Shanghai, and update my todo list.\nAssistant:\n{\"tool_calls\": [\n {\"name\": \"get_weather\", \"input\": {\"city\": \"Beijing\"}},\n {\"name\": \"get_weather\", \"input\": {\"city\": \"Shanghai\"}},\n {\"name\": \"update_todo\", \"input\": {\"todos\": [{\"content\": \"Buy milk\"}, {\"content\": \"Write report\"}]}}\n]}\n\nIMPORTANT:\n1) If calling tools, output ONLY the JSON object above. Do NOT include any extra text.\n2) Do NOT wrap tool-call JSON in markdown/code fences (for example, do not use triple backticks).\n3) After receiving a tool result, you MUST use it to produce the final answer.\n4) Only call another tool when the previous result is missing required data or returned an error.\n5) JSON SYNTAX STRICTLY REQUIRED: All property names MUST be enclosed in double quotes (e.g., \"name\", not name).\n6) ARRAY FORMAT: If providing a list of items, you MUST enclose them in square brackets `[]` (e.g., \"todos\": [{\"item\": \"a\"}, {\"item\": \"b\"}]). DO NOT output comma-separated objects without brackets."
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."
if policy.Mode == util.ToolChoiceRequired {
toolPrompt += "\n7) For this response, you MUST call at least one tool from the allowed list."
}

View File

@@ -77,10 +77,13 @@ func TestBuildOpenAIFinalPrompt_VercelPreparePathKeepsFinalAnswerInstruction(t *
if !strings.Contains(finalPrompt, "Only call another tool when the previous result is missing required data or returned an error.") {
t.Fatalf("vercel prepare finalPrompt missing retry guard instruction: %q", finalPrompt)
}
if !strings.Contains(finalPrompt, "Do NOT wrap tool-call JSON in markdown/code fences") {
t.Fatalf("vercel prepare finalPrompt missing no-fence instruction: %q", finalPrompt)
if !strings.Contains(finalPrompt, "output ONLY XML using this canonical format") {
t.Fatalf("vercel prepare finalPrompt missing xml format instruction: %q", finalPrompt)
}
if strings.Contains(finalPrompt, "```json") {
t.Fatalf("vercel prepare finalPrompt should not require fenced json tool calls: %q", finalPrompt)
if !strings.Contains(finalPrompt, "Do NOT wrap tool-call 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") {
t.Fatalf("vercel prepare finalPrompt should not require fenced tool calls: %q", finalPrompt)
}
}

View File

@@ -54,9 +54,9 @@ function parseToolCallsDetailed(text, toolNames) {
const candidates = buildToolCallCandidates(normalized);
let parsed = [];
for (const c of candidates) {
parsed = parseToolCallsPayload(c);
parsed = parseMarkupToolCalls(c);
if (parsed.length === 0) {
parsed = parseMarkupToolCalls(c);
parsed = parseToolCallsPayload(c);
}
if (parsed.length === 0) {
parsed = parseTextKVToolCalls(c);
@@ -101,9 +101,9 @@ function parseStandaloneToolCallsDetailed(text, toolNames) {
const candidates = buildToolCallCandidates(trimmed);
let parsed = [];
for (const c of candidates) {
parsed = parseToolCallsPayload(c);
parsed = parseMarkupToolCalls(c);
if (parsed.length === 0) {
parsed = parseMarkupToolCalls(c);
parsed = parseToolCallsPayload(c);
}
if (parsed.length === 0) {
parsed = parseTextKVToolCalls(c);

View File

@@ -34,13 +34,13 @@ func ParseToolCallsDetailed(text string, availableToolNames []string) ToolCallPa
candidates := buildToolCallCandidates(text)
var parsed []ParsedToolCall
for _, candidate := range candidates {
tc := parseToolCallsPayload(candidate)
if len(tc) == 0 {
tc = parseXMLToolCalls(candidate)
}
tc := parseXMLToolCalls(candidate)
if len(tc) == 0 {
tc = parseMarkupToolCalls(candidate)
}
if len(tc) == 0 {
tc = parseToolCallsPayload(candidate)
}
if len(tc) == 0 {
tc = parseTextKVToolCalls(candidate)
}
@@ -88,13 +88,13 @@ func ParseStandaloneToolCallsDetailed(text string, availableToolNames []string)
if candidate == "" {
continue
}
parsed = parseToolCallsPayload(candidate)
if len(parsed) == 0 {
parsed = parseXMLToolCalls(candidate)
}
parsed = parseXMLToolCalls(candidate)
if len(parsed) == 0 {
parsed = parseMarkupToolCalls(candidate)
}
if len(parsed) == 0 {
parsed = parseToolCallsPayload(candidate)
}
if len(parsed) == 0 {
parsed = parseTextKVToolCalls(candidate)
}