End-to-end walkthrough — create a Knowledge Base, then pass tool-call history through multi-turn API requests to avoid redundant retrieval
This guide shows how to pass tool-call history across turns so the model answers from prior retrieval results instead of re-running searches. The same sample document drives every step, so the results are reproducible.
The RAG Agent is stateless — each request stands alone and the server keeps no conversation history. A single response usually moves through several stages:If the next turn sends only the text history (the user question and the assistant's final answer), the model loses sight of the tool calls in between. It does not know what has already been retrieved, so it searches the same content again — wasting tokens and risking inconsistent answers.The fix is to pass the full tool-call chain (assistant.tool_calls plus the matching tool results) as history messages, so the model can answer from what it has already retrieved.
This guide uses the Model Studio docs sample bundle — 92 official Model Studio technical documents covering API usage, model references, and best practices. Download, unzip, and upload them to a Knowledge Base. The walkthrough uses First call to the Qwen API from the bundle to demonstrate follow-up questions:
Turn 1: Ask "How do I call the Qwen API?" (triggers multiple tool calls)
Turn 2: Follow up with "How do I set the API Key as an environment variable?" (verifies that history is reused and no extra retrieval runs)
You can substitute your own documents — the steps and outcome are the same.
Send the first request with the parameters you collected:
Copy
curl -X POST 'https://{workspaceId}.cn-beijing.maas.aliyuncs.com/api/v2/apps/knowledge/chat' \ -H 'Authorization: Bearer $API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "input": { "messages": [{"role": "user", "content": "How do I call the Qwen API"}] }, "parameters": { "agent_options": {"agent_id": "aid-xxxxxxxx"} }, "stream": true }'
The Agent runs several rounds of tool calls against the Knowledge Base, then streams back the final answer. You need to extract the tool-call history from the SSE stream — see Extract tool history from the SSE stream below.
For the follow-up "How do I set the API Key as an environment variable?", pass the full history from turn 1 (including the tool-call chain):
Copy
curl -X POST 'https://{workspaceId}.cn-beijing.maas.aliyuncs.com/api/v2/apps/knowledge/chat' \ -H 'Authorization: Bearer $API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "input": { "messages": [ {"role": "user", "content": "How do I call the Qwen API"}, {"role": "assistant", "content": "", "tool_calls": [{"id":"call_956...","function":{"name":"semantic_search",...}}]}, {"role": "tool", "tool_call_id": "call_956...", "content": "Retrieved 5 relevant chunks..."}, {"role": "assistant", "content": "Based on the retrieved First call to the Qwen API content..."}, {"role": "user", "content": "How do I set the API Key as an environment variable?"} ] }, "parameters": {"agent_options": {"agent_id": "aid-xxxxxxxx"}}, "stream": true }'
tool_call_id must be the exact value returned by the first turn's stream — do not invent one. The model uses it to match each call to its result.
Measured against First call to the Qwen API: after turn 1 asks "How do I call the Qwen API", turn 2 asks "How do I set the API Key as an environment variable?". Compare what happens with each history strategy:
New retrieval may hit different chunks, leading to drift
Measured numbers: turn 1 ran 2 rounds of tool calls (semantic_search → obtain_file), producing 6 history messages. Turn 2 with full history (7 messages) answered with 0 tool calls. Turn 2 with text-only history (3 messages) re-ran semantic_search twice. The exact counts depend on your document structure and how well the question matches, but the takeaway holds: full history eliminates redundant retrieval.
Full history makes the input longer (it carries tool results), but skips the retrieval round trip. Text-only history sends a smaller input but pays for retrieval again. In multi-turn follow-up scenarios, full history wins on both response speed and answer consistency.
The Agent streams output as SSE — each frame is data:{...}, with the message at obj.output.choices[0].message. message.extra.step_change marks state-change boundaries. You only need to handle three event types:
Event
Detection condition
Data to extract
Tool call
step_change == "tool_calling"
The tool_calls array (all parallel calls in this round)
The code below ties together "extract history from SSE → manage multi-turn history automatically → compare both approaches" in a single runnable script. The numbers shown were measured against the sample document and prompts on this page.
import jsonimport requests# Get these from the API Debugging page of your Q&A service in the console.API_URL = "https://{workspaceId}.cn-beijing.maas.aliyuncs.com/api/v2/apps/knowledge/chat"API_KEY = "sk-xxxxxxxx" # Workspace-scoped API Key (sk-ws- prefix)AGENT_ID = "aid-xxxxxxxx" # Q&A service ID
chat = MultiTurnChat()# Turn 1print("[Turn 1] How do I call the Qwen API")chat.ask("How do I call the Qwen API")# Turn 2 — full tool history (carried automatically)print("\n[Turn 2 - full tool history]")chat_full = MultiTurnChat()chat_full.history = list(chat.history)chat_full.ask("How do I set the API Key as an environment variable?")# Turn 2 — text-only history (drop tool calls, keep only user + assistant final answers)print("\n[Turn 2 - text-only history]")text_only = [m for m in chat.history if m["role"] == "user" or (m["role"] == "assistant" and not m.get("tool_calls"))]chat_text = MultiTurnChat()chat_text.history = text_onlychat_text.ask("How do I set the API Key as an environment variable?")
Measured output:
Copy
[Turn 1] How do I call the Qwen API Sent 1 messages, 2 tool-call round(s)[Turn 2 - full tool history] Sent 7 messages, 0 tool-call round(s) <- reused prior results, no extra retrieval[Turn 2 - text-only history] Sent 3 messages, 2 tool-call round(s) <- retrieval ran again
Tool returns can be long. After several turns the total may exceed the context limit. Recommendations:
Truncate earlier tool returns: keep full tool content only for the most recent K turns. For older turns, truncate the tool message to the first N characters.
def truncate_history(messages, max_tool_len=2000, keep_recent=2): """Truncate tool returns from earlier turns.""" user_indices = [i for i, m in enumerate(messages) if m["role"] == "user"] if len(user_indices) <= keep_recent: return messages cutoff = user_indices[-keep_recent] result = [] for i, msg in enumerate(messages): if i < cutoff and msg["role"] == "tool" and len(msg["content"]) > max_tool_len: msg = {**msg, "content": msg["content"][:max_tool_len] + "\n...(truncated)"} result.append(msg) return result
What happens if I do not send tool-call history?
The model loses the retrieval context, re-searches the same content, and answer quality drops. Measured: with full history, the follow-up runs 0 tool calls and answers immediately; without it, retrieval runs again.Can I skip the intermediate tool calls and send only the final answer?
It works, but it is not recommended. Sending only user + assistant(final answer) strips the model's memory of the Knowledge Base content, and follow-up quality noticeably drops.Does the order of parallel tool returns matter?
The order does not affect function — the model matches by tool_call_id. But every tool message must immediately follow its corresponding assistant(tool_calls) message.