不论是 Claude、OpenAI、Gemini 还是 Doubao,在 Agent 的基本工作方式上其实非常相似。模型调用、上下文数据格式以及 Tool 的引用方式虽然在具体 SDK 上有所差异,但整体流程基本一致:
每次调用 LLM 时,都需要提供当前的上下文(Context)以及模型当前可以使用的工具(Tools)。
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| tools_schema = [ { "type": "function", "name": "get_weather", "description": "获取指定城市的实时天气信息", "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "城市名称,例如:北京、上海" } }, "required": ["location"] } } ]
response = client.responses.create( model="deepseek-v4-flash", instructions="你是一个对话机器人", input=messages, tools=tools_schema, stream=False, extra_body={ "reasoning": {"effort": "none"}, } )
|
当模型判断需要调用某个工具时,并不会直接执行工具,而是在响应中返回一个 type="function_call" 的项。
服务端接收到这个 function_call 后,需要完成以下几步:
- 将模型返回的 Tool Call 加入上下文;
- 根据模型提供的函数名和参数,实际执行对应的工具;
- 将工具执行结果再次加入上下文;
- 把更新后的上下文重新发送给 LLM。
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| tool_calls = [ item for item in response.output if item.type == "function_call" ]
if tool_calls: messages.extend(response.output)
for tool_call in tool_calls: func_name = tool_call.name func_args = json.loads(tool_call.arguments)
print( f"[info] 模型决定调用工具: " f"{func_name},参数: {func_args}" )
result = None
if func_name == "get_weather": result = get_weather( location=func_args.get("location") )
print( f"[info] 工具 {func_name} 运行结果: " f"{json.loads(result)}" )
messages.append({ "type": "function_call_output", "call_id": tool_call.call_id, "output": str(result) })
|
模型拿到工具返回的结果后,会基于新的上下文继续进行推理:
- 如果还需要更多信息,就继续调用工具;
- 如果已经获得足够的信息,就生成最终回答。
因此,Agent 的核心其实就是这样一个不断迭代的循环:
LLM → Tool Call → Tool Execution → Tool Result → LLM → …
从更经典的 Agent 视角来看,这对应 ReAct 论文中所描述的:
Thought → Action → Observation → Thought → …
也就是说,所谓的 Agent Loop,本质上就是让模型在推理、行动、观察结果之间不断循环,直到模型认为当前任务已经完成。