AI Agent 30天速成|Day2 学习笔记

AI Agent 全日制30天速成|Day2 教学笔记

今日总学习目标

  1. 理解Function Calling 函数调用核心原理、执行流程与调用规范
  2. 基于Day1异步LLM客户端,实现通用工具调用框架(单工具/多工具串行调用)
  3. 掌握多轮对话上下文管理,实现会话记忆、历史消息拼接、Token 预估裁剪
  4. 学会工具调用异常处理、参数校验、失败重试、结果回填逻辑
    每日时长分配(全天8h)
  • 理论笔记阅读+理解:2h
  • 代码编写调试:4.5h
  • 复盘+面试题背诵:1.5h

一、核心理论教学笔记

1. Function Calling 函数调用(Agent 核心能力)

1.1 核心概念

Function Calling 是大模型主动识别意图、选择工具、生成调用参数的能力。
模型不再只输出自然语言,而是根据预设工具描述,判断是否需要调用外部函数/接口,并输出标准化工具调用格式,由程序执行工具、拿回结果再回传给模型,形成闭环。

适用场景:实时数据查询、计算器、查天气、数据库查询、接口请求、代码执行、知识库检索等模型知识盲区场景。

1.2 完整执行流程(标准4步)

  1. 用户提问 + 传入工具列表:将工具名称、功能描述、入参Schema 随对话一并传给大模型
  2. 模型判断并输出工具调用参数:识别意图,选择对应工具,生成合法入参,返回指定格式调用体
  3. 本地代码解析并执行工具:程序解析调用结果,路由到对应函数,执行业务逻辑
  4. 工具结果回填对话:把工具返回内容拼接进历史消息,再次请求LLM,模型结合结果给出最终回答

1.3 工具定义三要素(必填)

所有大模型兼容 OpenAI 工具定义规范,三要素缺一不可:

  • name:工具函数名称(唯一标识)
  • description:工具功能描述(决定模型会不会选这个工具,描述必须精准)
  • parameters:入参 JSON Schema(字段名、类型、是否必填、字段说明)

1.4 关键规则与限制

  1. 必须配合结构化输出,依赖 JSON 解析,temperature 建议设 0.0~0.1
  2. 工具描述、参数说明直接影响模型选工具、填参数的准确率,描述越详细效果越好
  3. 单次支持单工具调用、部分模型支持并行多工具调用,入门优先实现串行调用
  4. 工具调用消息会占用 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 上下文两大核心问题

  1. 消息累积过长:多轮对话后消息越来越多,快速触达上下文窗口上限
  2. Token 超限:历史消息+当前提问+工具内容总和超模型最大 Token,导致截断、失忆、报错

2.3 基础 Token 预估策略(入门方案)

沿用 Day1 规则:

  • 中文汉字:≈ 2 Token / 字
  • 英文、符号、数字:≈ 1 Token / 字符
    简易预估:遍历所有历史消息内容,累加字符数做换算,低成本实现预警与裁剪。

2.4 上下文裁剪方案(入门优先)

  1. 滑动窗口截断:保留 system 消息 + 最近 N 轮对话,删除最早历史(最简单、工程最常用)
  2. 阈值触发裁剪:设置安全 Token 阈值,超过阈值自动裁剪旧消息
  3. 保留关键消息:永远保留 system 人设、工具调用关键消息,只删减闲聊历史

3. 工具调用异常分类与处理策略

异常类型 现象 处理方案
模型选错工具 调用了不匹配当前问题的函数 优化工具描述 + 增加 Few-shot 示例
入参格式错误 参数缺失、类型错误、字段不存在 Pydantic 强制参数校验,校验失败拒绝执行并回传错误
工具执行报错 函数内部代码/接口请求异常 捕获异常,将错误信息回填上下文,让模型重新应答
循环调用工具 工具返回后模型反复重复调用同一工具 增加调用次数上限,超过上限强制终止工具链
Token 超限 消息列表过长触发报错 提前预估 Token,自动裁剪历史消息

4. 异步框架适配要点

基于 Day1 AsyncLLMClient 改造:

  1. 新增 tools 参数,支持传入工具列表定义
  2. 新增工具调用专用解析逻辑,区分「普通回答」和「工具调用响应」
  3. 封装统一多轮对话管理器,自动维护消息队列
  4. 全链路异步:工具函数、消息管理、LLM 请求全部使用 async

二、今日学习重点

  1. 掌握标准 Function Calling 消息格式、工具 Schema 编写
  2. 基于昨日代码改造,实现通用异步工具调用框架
  3. 实现多轮对话消息管理、Token 预估、滑动窗口裁剪
  4. 完成「提问→调工具→回传结果→模型总结」完整闭环
  5. 实现工具参数校验、执行异常捕获、调用次数限制

三、今日难点 & 解决方案

难点1:模型频繁选错工具、参数填错

解决方案:

  1. 优化 description,写清适用场景、禁止场景
  2. 给工具入参补充详细注释、示例值
  3. 在 system 提示词中约束:优先选择对应工具,无匹配工具直接回答
  4. 增加 1~2 条少样本调用示例,对齐模型行为

难点2:多轮消息无限累积,频繁 Token 超限

解决方案:

  1. 全局设置 Token 安全阈值,每次请求前做预估
  2. 固定保留 system 消息,采用滑动窗口删除最早会话
  3. 精简工具返回内容,剔除冗余文本、换行、空格

难点3:工具调用死循环(反复调用同一工具)

解决方案:

  1. 单轮会话设置最大工具调用轮次(如最多 3 轮)
  2. 达到上限后强制停止工具调用,让模型直接基于已有结果作答
  3. 工具返回内容做总结压缩,避免重复触发调用意图

难点4:工具执行异常后,整个对话流程中断

解决方案:

  1. 工具函数内部 try/except 捕获所有异常,统一返回错误文本
  2. 将错误内容以 tool 角色回填消息列表,交由模型处理
  3. 增加单次工具调用重试次数(最多1次)

四、完整练习代码(基于Day1代码扩展)

前置依赖

延续昨日环境,已安装:aiohttppydanticfastapiuvicorn

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)

五、今日必做练习任务

  1. 替换合法 API Key,运行 llm_client_v2.py,观察工具调用完整流程
  2. 修改用户提问,测试加法、减法、除法、除零异常,查看错误处理逻辑
  3. 调整 token_thresholdkeep_last_round,验证自动裁剪上下文效果
  4. 启动 FastAPI,访问 /docs 接口页面,连续多轮提问,观察历史消息与 Token 变化
  5. 修改工具描述,故意写模糊内容,观察模型是否选错工具,再优化描述对比效果
  6. 调高 max_tool_round,测试循环调用限制是否生效

六、今日配套面试题(Agent 开发高频)

基础问答

  1. 简述 Function Calling 完整执行流程,一共分为哪几步?
  2. 定义工具时,namedescriptionparameters 各自作用是什么?哪一部分对模型选工具影响最大?
  3. 多轮对话为什么会出现 Token 超限?简单说下你的解决方案。
  4. system、user、assistant、tool 四种消息角色分别代表什么含义?
  5. 工具调用场景为什么 temperature 要设置为 0 左右?

工程实操题

  1. 模型调用工具后出现死循环、反复调用同一工具,如何解决?
  2. 工具返回报错信息(参数错误、执行异常),代码层面如何处理才能保证对话不中断?
  3. 简述滑动窗口裁剪上下文的实现思路,优缺点是什么?
  4. 如何保证模型生成的工具入参合法、符合约定格式?

拓展思考题(进阶)

  1. 目前代码是全局单会话,线上多用户场景如何实现会话隔离?有哪些存储方案?
  2. 除了滑动窗口,还有哪些高级上下文压缩方案?各自适用什么场景?
  3. 如何实现并行多工具调用?和串行调用的区别与适用场景?

面试题参考答案

基础问答

  1. Function Calling 执行流程
    ① 传入用户问题 + 工具列表给 LLM;② 模型识别意图,输出工具名称与入参;③ 本地代码解析参数、校验并执行工具;④ 将工具结果以 tool 角色回填上下文,再次请求 LLM;⑤ 模型结合工具结果给出最终自然语言回答。

  2. 工具三要素作用

  • name:工具唯一标识,用于路由到对应函数;
  • description:工具功能、适用场景描述,直接决定模型是否选择该工具,影响最大
  • parameters:入参 JSON Schema,约束字段名、类型、必填项,保证参数合法。
  1. Token 超限原因与方案
    多轮对话不断累积历史消息,输入总 Token 超过模型上下文窗口。
    方案:提前预估 Token、设置阈值、滑动窗口裁剪历史、对话摘要压缩。

  2. 消息角色含义

  • system:系统人设、全局规则;
  • user:用户输入;
  • assistant:模型回答 / 模型发起工具调用;
  • tool:外部工具执行后的返回结果。
  1. 工具调用 temperature 设为0的原因
    工具调用要求参数、工具名称完全精准,低温度消除随机性,避免模型乱填参数、选错工具,保证调用成功率。

工程实操题

  1. 解决工具调用死循环
    设置单轮会话最大工具调用次数,达到上限强制停止调用,让模型直接作答;优化工具描述与提示词,减少重复触发条件;精简工具返回内容。

  2. 工具异常处理
    工具内部 try/except 捕获所有异常,统一封装错误文本;将错误内容作为 tool 消息回填上下文,交由模型理解并重新应答;增加参数预校验,提前拦截非法参数。

  3. 滑动窗口思路与优缺点
    思路:永远保留 system 消息,只保留最近 N 轮对话,删除最早历史。
    优点:实现简单、性能高、无额外 Token 消耗;缺点:会丢失早期历史对话信息,适合普通闲聊、短会话。

  4. 保证入参格式合法
    使用 Pydantic 定义参数模型做强制校验;提示词约束参数格式;解析 JSON 异常时捕获错误并回填,让模型重新生成参数。

拓展思考题

  1. 多用户会话隔离
    为每个用户分配唯一会话 ID;会话数据存入 Redis、MySQL 等中间件;接口根据会话 ID 读写独立消息列表,实现用户隔离。

  2. 高级上下文压缩方案

  • 对话摘要:用 LLM 把早期多轮对话压缩成一段摘要,替代原始消息,保留语义;
  • RAG 检索:历史对话向量化存储,只召回和当前问题相关片段;
  • 分级记忆:区分短期记忆(原始消息)、长期记忆(摘要/结构化数据)。
  1. 并行多工具调用
    模型一次返回多个 tool_calls,程序并行执行多个工具,全部完成后统一回填结果。
    串行:逐个调用工具,流程简单、易调试,适合依赖型工具;并行:效率更高,适合多个互不依赖的独立查询工具。

学习总结

Day2 在 Day1 异步 LLM 客户端基础上,完成了 Agent 两大核心能力:函数调用多轮会话管理
Function Calling 是 AI Agent 联动外部能力的核心,上下文管理是保证多轮对话稳定运行的基础,这两部分也是面试和工程落地的重点。建议反复调试工具调用异常、上下文裁剪逻辑,为后续复杂 Agent 编排、记忆模块、RAG 串联打下基础。

posted @ 2026-06-17 20:28  云淡风轻YangG  阅读(76)  评论(0)    收藏  举报