AI Agent 30天速成|Day2 学习笔记
AI Agent 全日制30天速成|Day2 教学笔记
今日总学习目标
- 理解Function Calling 函数调用核心原理、执行流程与调用规范
- 基于Day1异步LLM客户端,实现通用工具调用框架(单工具/多工具串行调用)
- 掌握多轮对话上下文管理,实现会话记忆、历史消息拼接、Token 预估裁剪
- 学会工具调用异常处理、参数校验、失败重试、结果回填逻辑
每日时长分配(全天8h)
- 理论笔记阅读+理解:2h
- 代码编写调试:4.5h
- 复盘+面试题背诵:1.5h
一、核心理论教学笔记
1. Function Calling 函数调用(Agent 核心能力)
1.1 核心概念
Function Calling 是大模型主动识别意图、选择工具、生成调用参数的能力。
模型不再只输出自然语言,而是根据预设工具描述,判断是否需要调用外部函数/接口,并输出标准化工具调用格式,由程序执行工具、拿回结果再回传给模型,形成闭环。
适用场景:实时数据查询、计算器、查天气、数据库查询、接口请求、代码执行、知识库检索等模型知识盲区场景。
1.2 完整执行流程(标准4步)
- 用户提问 + 传入工具列表:将工具名称、功能描述、入参Schema 随对话一并传给大模型
- 模型判断并输出工具调用参数:识别意图,选择对应工具,生成合法入参,返回指定格式调用体
- 本地代码解析并执行工具:程序解析调用结果,路由到对应函数,执行业务逻辑
- 工具结果回填对话:把工具返回内容拼接进历史消息,再次请求LLM,模型结合结果给出最终回答
1.3 工具定义三要素(必填)
所有大模型兼容 OpenAI 工具定义规范,三要素缺一不可:
name:工具函数名称(唯一标识)description:工具功能描述(决定模型会不会选这个工具,描述必须精准)parameters:入参 JSON Schema(字段名、类型、是否必填、字段说明)
1.4 关键规则与限制
- 必须配合结构化输出,依赖 JSON 解析,
temperature建议设0.0~0.1 - 工具描述、参数说明直接影响模型选工具、填参数的准确率,描述越详细效果越好
- 单次支持单工具调用、部分模型支持并行多工具调用,入门优先实现串行调用
- 工具调用消息会占用 Token,多轮会话需统一做 Token 统计与裁剪
2. 多轮对话 & 上下文管理
2.1 消息结构规范
统一遵循 OpenAI 消息体格式,角色固定三类:
system:系统提示词(人设、规则、全局约束,全局生效)user:用户提问assistant:模型回答 / 模型工具调用请求tool:工具执行返回结果(标准角色,用于回填上下文)
消息列表示例:
[
{"role":"system", "content":"你是智能助手,可调用工具计算数学题"},
{"role":"user", "content":"123 + 456 等于多少?"},
{"role":"assistant", "tool_calls": [...]},
{"role":"tool", "tool_call_id":"xxx", "content":"计算结果:579"}
]
2.2 上下文两大核心问题
- 消息累积过长:多轮对话后消息越来越多,快速触达上下文窗口上限
- Token 超限:历史消息+当前提问+工具内容总和超模型最大 Token,导致截断、失忆、报错
2.3 基础 Token 预估策略(入门方案)
沿用 Day1 规则:
- 中文汉字:≈ 2 Token / 字
- 英文、符号、数字:≈ 1 Token / 字符
简易预估:遍历所有历史消息内容,累加字符数做换算,低成本实现预警与裁剪。
2.4 上下文裁剪方案(入门优先)
- 滑动窗口截断:保留
system消息 + 最近 N 轮对话,删除最早历史(最简单、工程最常用) - 阈值触发裁剪:设置安全 Token 阈值,超过阈值自动裁剪旧消息
- 保留关键消息:永远保留 system 人设、工具调用关键消息,只删减闲聊历史
3. 工具调用异常分类与处理策略
| 异常类型 | 现象 | 处理方案 |
|---|---|---|
| 模型选错工具 | 调用了不匹配当前问题的函数 | 优化工具描述 + 增加 Few-shot 示例 |
| 入参格式错误 | 参数缺失、类型错误、字段不存在 | Pydantic 强制参数校验,校验失败拒绝执行并回传错误 |
| 工具执行报错 | 函数内部代码/接口请求异常 | 捕获异常,将错误信息回填上下文,让模型重新应答 |
| 循环调用工具 | 工具返回后模型反复重复调用同一工具 | 增加调用次数上限,超过上限强制终止工具链 |
| Token 超限 | 消息列表过长触发报错 | 提前预估 Token,自动裁剪历史消息 |
4. 异步框架适配要点
基于 Day1 AsyncLLMClient 改造:
- 新增
tools参数,支持传入工具列表定义 - 新增工具调用专用解析逻辑,区分「普通回答」和「工具调用响应」
- 封装统一多轮对话管理器,自动维护消息队列
- 全链路异步:工具函数、消息管理、LLM 请求全部使用 async
二、今日学习重点
- 掌握标准 Function Calling 消息格式、工具 Schema 编写
- 基于昨日代码改造,实现通用异步工具调用框架
- 实现多轮对话消息管理、Token 预估、滑动窗口裁剪
- 完成「提问→调工具→回传结果→模型总结」完整闭环
- 实现工具参数校验、执行异常捕获、调用次数限制
三、今日难点 & 解决方案
难点1:模型频繁选错工具、参数填错
解决方案:
- 优化
description,写清适用场景、禁止场景 - 给工具入参补充详细注释、示例值
- 在 system 提示词中约束:优先选择对应工具,无匹配工具直接回答
- 增加 1~2 条少样本调用示例,对齐模型行为
难点2:多轮消息无限累积,频繁 Token 超限
解决方案:
- 全局设置 Token 安全阈值,每次请求前做预估
- 固定保留 system 消息,采用滑动窗口删除最早会话
- 精简工具返回内容,剔除冗余文本、换行、空格
难点3:工具调用死循环(反复调用同一工具)
解决方案:
- 单轮会话设置最大工具调用轮次(如最多 3 轮)
- 达到上限后强制停止工具调用,让模型直接基于已有结果作答
- 工具返回内容做总结压缩,避免重复触发调用意图
难点4:工具执行异常后,整个对话流程中断
解决方案:
- 工具函数内部 try/except 捕获所有异常,统一返回错误文本
- 将错误内容以
tool角色回填消息列表,交由模型处理 - 增加单次工具调用重试次数(最多1次)
四、完整练习代码(基于Day1代码扩展)
前置依赖
延续昨日环境,已安装:aiohttp、pydantic、fastapi、uvicorn
1. 新增工具函数定义 + 增强版LLM客户端 llm_client_v2.py
import aiohttp
import asyncio
import re
import json
from pydantic import BaseModel, Field, ValidationError
from typing import Optional, AsyncGenerator, List, Dict, Any
# ========== 1. 模型配置(沿用Day1) ==========
MODEL_CONFIG = {
"qwen-turbo": {
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions",
"api_key": "你的通义千问key"
},
"deepseek-chat": {
"base_url": "https://api.deepseek.com/v1/chat/completions",
"api_key": "你的deepseek key"
}
}
# ========== 2. 工具参数Schema & 工具函数定义 ==========
# 示例1:计算器工具参数
class CalcToolParams(BaseModel):
num1: float = Field(description="第一个运算数字")
num2: float = Field(description="第二个运算数字")
op: str = Field(description="运算符号,仅支持:+、-、*、/")
# 工具列表定义(标准OpenAI tools 格式)
TOOLS = [
{
"type": "function",
"function": {
"name": "calculator",
"description": "数学计算器,用于加减乘除运算,仅在用户提出数学计算时使用",
"parameters": CalcToolParams.model_json_schema()
}
}
]
# 异步工具函数映射
async def calculator(num1: float, num2: float, op: str) -> str:
"""异步计算器工具"""
try:
if op == "+":
res = num1 + num2
elif op == "-":
res = num1 - num2
elif op == "*":
res = num1 * num2
elif op == "/":
if num2 == 0:
return "计算失败:除数不能为0"
res = num1 / num2
else:
return f"计算失败:不支持运算符 {op}"
return f"计算结果:{num1} {op} {num2} = {res}"
except Exception as e:
return f"工具执行异常:{str(e)}"
TOOL_MAP = {
"calculator": calculator
}
# ========== 3. 增强版异步LLM客户端(支持Function Calling) ==========
class AsyncLLMClientV2:
def __init__(self, model_name: str = "qwen-turbo", max_concurrent: int = 5):
self.model = model_name
self.conf = MODEL_CONFIG[model_name]
self.semaphore = asyncio.Semaphore(max_concurrent)
self.max_tool_round = 3 # 最大工具调用轮次,防止死循环
async def _request(self, messages: List[Dict[str, str]], tools: List[Dict] = None, stream: bool = False):
headers = {
"Authorization": f"Bearer {self.conf['api_key']}",
"Content-Type": "application/json"
}
timeout = aiohttp.ClientTimeout(total=60)
payload = {
"model": self.model,
"messages": messages,
"temperature": 0.0,
"stream": stream
}
if tools:
payload["tools"] = tools
async with self.semaphore:
async with aiohttp.ClientSession(timeout=timeout) as session:
try:
async with session.post(self.conf["base_url"], json=payload, headers=headers) as resp:
if resp.status != 200:
err = await resp.text()
raise Exception(f"接口异常 {resp.status}: {err}")
if stream:
return self._stream_parse(resp)
return await resp.json()
except aiohttp.ClientError as e:
raise Exception(f"网络请求异常: {str(e)}")
async def _stream_parse(self, response) -> AsyncGenerator[str, None]:
buffer = ""
async for chunk in response.content.iter_chunked(1024):
buffer += chunk.decode("utf-8")
while "data:" in buffer:
idx = buffer.find("data:")
end_idx = buffer.find("\n\n", idx)
if end_idx == -1:
break
data_block = buffer[idx+5:end_idx].strip()
buffer = buffer[end_idx+2:]
if data_block == "[DONE]":
return
try:
data = json.loads(data_block)
content = data["choices"][0]["delta"].get("content", "")
if content:
yield content
except:
continue
# 基础对话(无工具)
async def chat(self, messages: List[Dict[str, str]]) -> str:
resp = await self._request(messages)
return resp["choices"][0]["message"]["content"]
# 核心:自动工具调用闭环
async def chat_with_tools(self, messages: List[Dict[str, str]], tools: List[Dict]) -> str:
tool_round = 0
while tool_round < self.max_tool_round:
tool_round += 1
resp = await self._request(messages, tools=tools)
msg = resp["choices"][0]["message"]
# 分支1:模型选择调用工具
if "tool_calls" in msg and msg["tool_calls"]:
tool_call = msg["tool_calls"][0]
func_name = tool_call["function"]["name"]
func_args = tool_call["function"]["arguments"]
call_id = tool_call["id"]
# 解析参数 + Pydantic校验
try:
args_dict = json.loads(func_args)
params = CalcToolParams(**args_dict)
except (json.JSONDecodeError, ValidationError):
err_msg = f"工具参数解析失败,原始参数:{func_args}"
messages.append({
"role": "tool",
"tool_call_id": call_id,
"content": err_msg
})
continue
# 执行对应工具
if func_name not in TOOL_MAP:
exec_res = f"错误:不存在工具 {func_name}"
else:
exec_res = await TOOL_MAP[func_name](**params.model_dump())
# 回填工具结果到上下文
messages.append(msg)
messages.append({
"role": "tool",
"tool_call_id": call_id,
"content": exec_res
})
# 分支2:不再调用工具,返回最终回答
else:
return msg["content"]
# 超过最大轮次,直接返回最后结果
final_resp = await self._request(messages)
return final_resp["choices"][0]["message"]["content"]
# ========== 4. 多轮对话管理器(上下文 + Token 简易裁剪) ==========
class ChatSession:
def __init__(self, system_prompt: str):
self.messages = [{"role": "system", "content": system_prompt}]
self.token_threshold = 2000 # Token 安全阈值
self.keep_last_round = 4 # 保留最近4轮对话
# 简易Token预估
def estimate_token(self) -> int:
total = 0
for msg in self.messages:
text = msg.get("content", "")
total += len(text) * 2
return total
# 滑动窗口裁剪
def trim_messages(self):
if self.estimate_token() > self.token_threshold:
# 保留 system + 最近N轮,删除最早消息
keep = [self.messages[0]] + self.messages[-self.keep_last_round:]
self.messages = keep
# 添加消息
def add_message(self, role: str, content: str):
self.messages.append({"role": role, "content": content})
self.trim_messages()
# ========== 测试入口 ==========
async def test_tool_chat():
# 1. 初始化会话与客户端
session = ChatSession(system_prompt="你是具备计算器能力的智能助手,数学题必须调用计算器工具。")
client = AsyncLLMClientV2("qwen-turbo")
# 2. 用户提问
user_q = "1024 乘以 8 等于多少?"
session.add_message("user", user_q)
print(f"用户提问:{user_q}")
# 3. 执行工具调用闭环
ans = await client.chat_with_tools(session.messages, TOOLS)
print(f"最终回答:{ans}")
# 多轮测试
user_q2 = "再帮我算 999 - 333"
session.add_message("user", user_q2)
print(f"\n用户提问:{user_q2}")
ans2 = await client.chat_with_tools(session.messages, TOOLS)
print(f"最终回答:{ans2}")
if __name__ == "__main__":
asyncio.run(test_tool_chat())
2. FastAPI 多轮+工具调用接口 main_v2.py
from fastapi import FastAPI, Query
import asyncio
from llm_client_v2 import AsyncLLMClientV2, ChatSession, TOOLS
app = FastAPI(title="Day2 Function Calling & 多轮对话")
client = AsyncLLMClientV2("qwen-turbo")
# 全局会话(演示用,生产改用Redis/数据库存储会话)
global_session = ChatSession(system_prompt="你是智能助手,数学计算请使用计算器工具。")
@app.get("/chat/tool")
async def chat_with_tool(prompt: str = Query(..., description="用户提问")):
global_session.add_message("user", prompt)
result = await client.chat_with_tools(global_session.messages, TOOLS)
return {
"answer": result,
"current_token_estimate": global_session.estimate_token(),
"history_count": len(global_session.messages)
}
if __name__ == "__main__":
import uvicorn
uvicorn.run("main_v2:app", reload=True)
五、今日必做练习任务
- 替换合法 API Key,运行
llm_client_v2.py,观察工具调用完整流程 - 修改用户提问,测试加法、减法、除法、除零异常,查看错误处理逻辑
- 调整
token_threshold和keep_last_round,验证自动裁剪上下文效果 - 启动 FastAPI,访问
/docs接口页面,连续多轮提问,观察历史消息与 Token 变化 - 修改工具描述,故意写模糊内容,观察模型是否选错工具,再优化描述对比效果
- 调高
max_tool_round,测试循环调用限制是否生效
六、今日配套面试题(Agent 开发高频)
基础问答
- 简述 Function Calling 完整执行流程,一共分为哪几步?
- 定义工具时,
name、description、parameters各自作用是什么?哪一部分对模型选工具影响最大? - 多轮对话为什么会出现 Token 超限?简单说下你的解决方案。
- system、user、assistant、tool 四种消息角色分别代表什么含义?
- 工具调用场景为什么
temperature要设置为 0 左右?
工程实操题
- 模型调用工具后出现死循环、反复调用同一工具,如何解决?
- 工具返回报错信息(参数错误、执行异常),代码层面如何处理才能保证对话不中断?
- 简述滑动窗口裁剪上下文的实现思路,优缺点是什么?
- 如何保证模型生成的工具入参合法、符合约定格式?
拓展思考题(进阶)
- 目前代码是全局单会话,线上多用户场景如何实现会话隔离?有哪些存储方案?
- 除了滑动窗口,还有哪些高级上下文压缩方案?各自适用什么场景?
- 如何实现并行多工具调用?和串行调用的区别与适用场景?
面试题参考答案
基础问答
-
Function Calling 执行流程
① 传入用户问题 + 工具列表给 LLM;② 模型识别意图,输出工具名称与入参;③ 本地代码解析参数、校验并执行工具;④ 将工具结果以 tool 角色回填上下文,再次请求 LLM;⑤ 模型结合工具结果给出最终自然语言回答。 -
工具三要素作用
name:工具唯一标识,用于路由到对应函数;description:工具功能、适用场景描述,直接决定模型是否选择该工具,影响最大;parameters:入参 JSON Schema,约束字段名、类型、必填项,保证参数合法。
-
Token 超限原因与方案
多轮对话不断累积历史消息,输入总 Token 超过模型上下文窗口。
方案:提前预估 Token、设置阈值、滑动窗口裁剪历史、对话摘要压缩。 -
消息角色含义
- system:系统人设、全局规则;
- user:用户输入;
- assistant:模型回答 / 模型发起工具调用;
- tool:外部工具执行后的返回结果。
- 工具调用 temperature 设为0的原因
工具调用要求参数、工具名称完全精准,低温度消除随机性,避免模型乱填参数、选错工具,保证调用成功率。
工程实操题
-
解决工具调用死循环
设置单轮会话最大工具调用次数,达到上限强制停止调用,让模型直接作答;优化工具描述与提示词,减少重复触发条件;精简工具返回内容。 -
工具异常处理
工具内部 try/except 捕获所有异常,统一封装错误文本;将错误内容作为 tool 消息回填上下文,交由模型理解并重新应答;增加参数预校验,提前拦截非法参数。 -
滑动窗口思路与优缺点
思路:永远保留 system 消息,只保留最近 N 轮对话,删除最早历史。
优点:实现简单、性能高、无额外 Token 消耗;缺点:会丢失早期历史对话信息,适合普通闲聊、短会话。 -
保证入参格式合法
使用 Pydantic 定义参数模型做强制校验;提示词约束参数格式;解析 JSON 异常时捕获错误并回填,让模型重新生成参数。
拓展思考题
-
多用户会话隔离
为每个用户分配唯一会话 ID;会话数据存入 Redis、MySQL 等中间件;接口根据会话 ID 读写独立消息列表,实现用户隔离。 -
高级上下文压缩方案
- 对话摘要:用 LLM 把早期多轮对话压缩成一段摘要,替代原始消息,保留语义;
- RAG 检索:历史对话向量化存储,只召回和当前问题相关片段;
- 分级记忆:区分短期记忆(原始消息)、长期记忆(摘要/结构化数据)。
- 并行多工具调用
模型一次返回多个 tool_calls,程序并行执行多个工具,全部完成后统一回填结果。
串行:逐个调用工具,流程简单、易调试,适合依赖型工具;并行:效率更高,适合多个互不依赖的独立查询工具。
学习总结
Day2 在 Day1 异步 LLM 客户端基础上,完成了 Agent 两大核心能力:函数调用与多轮会话管理。
Function Calling 是 AI Agent 联动外部能力的核心,上下文管理是保证多轮对话稳定运行的基础,这两部分也是面试和工程落地的重点。建议反复调试工具调用异常、上下文裁剪逻辑,为后续复杂 Agent 编排、记忆模块、RAG 串联打下基础。

浙公网安备 33010602011771号