目 录CONTENT

文章目录

agent学习笔记-场景(一)

~梓
2026-06-29 / 0 评论 / 0 点赞 / 2 阅读 / 0 字
温馨提示:
部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

Agent 学习笔记

通过调试一个带工具调用的 Agent,记录 Agent 的核心机制、工具选择原理、会话记忆行为与调试方法。结论均来自实际实验,场景已抽象为通用描述,可迁移到任意 LangChain/LangGraph Agent 项目。


一、一个 Agent 通常有两类实现

高层封装型 底层手搓型
编排方式 框架的工厂函数(如 create_agent 状态图(如 StateGraph
范式 ReAct(思考→调工具→看结果→再思考) Plan-Execute-Replan(规划→执行→重规划)
提示工程 薄(一句系统提示) 厚(每个节点各有 prompt + 输出模板 + 动态注入)
代码量
灵活度 低(框架包办循环) 高(每步可控)
适合场景 简单问答 多步骤、要可控、要结构化产出

设计原则:简单任务用高层封装,复杂任务用底层手搓。


二、Agent 的三层结构

一个 Agent 的构造通常有三个核心参数,对应三层:

model        → 大脑(思考、决定调不调工具、生成回答)
tools        → 手脚(实际执行操作,靠描述被 LLM 识别)
checkpointer → 记忆(按会话 ID 存取对话历史)
  • 大脑:任一支持 function calling 的 LLM(OpenAI、通义、DeepSeek 等)。模型必须支持工具调用,否则 agent 不成立。
  • 手脚:工具,必须是框架的 Tool 类型(如 LangChain 的 BaseTool)才能绑定给 LLM。
  • 记忆checkpointer,保存对话状态,让多轮对话有上下文。

三、ReAct 与 RAG 的关系(易混淆)

这俩是不同维度的概念:

  • ReAct:Agent 的运行范式——思考→调工具→看结果→再思考→回答的循环。
  • RAG:一种能力——检索知识库文档喂给 LLM,体现为一个「检索工具」。

一个 agent 可以是「用 ReAct 范式实现,其中挂了一个 RAG 检索工具」。两者不冲突:ReAct 是骨架,RAG 是其中一个工具的能力。

举例:一个 agent 有两个工具——「查当前时间」和「检索文档」。问时间时走 ReAct 但不走 RAG;问文档内容时既走 ReAct(作为工具被调)又走 RAG(内部检索)。


四、高层封装与底层引擎的关系

不是「高层型用封装库、底层型用图库」——两者底层都是同一个图引擎。

验证:高层工厂函数(如 create_agent)返回的对象类型是 CompiledStateGraph(图引擎的编译图)。

高层 API(create_agent、tools、messages)
        │  底层基于
        ▼
图引擎(StateGraph、节点、边、checkpointer)
  • 高层工厂函数是「快捷方式」,内部调图引擎自动搭好 ReAct 图(agent 节点 + tools 节点 + 循环边)。
  • 底层手搓不走快捷方式,直接用图引擎定义节点和边,更灵活、代码更多。

五、工具系统

5.1 工具装饰器

from langchain_core.tools import tool

@tool
def get_weather(city: str = "Beijing") -> str:
    """查询指定城市的实时天气。当用户问"天气/下雨/温度"时使用。"""
    ...

装饰器把普通函数变成工具。LLM 收到的是工具的 schema:

name:        get_weather
description: 查询指定城市的实时天气...   ← 来自 docstring
args:        { city: string, 默认 "Beijing" }

5.2 docstring 不是注释(关键区分)

写法 运行时存在 谁能看
注释 # xxx 否,编译时丢弃 只有人
docstring """xxx"""(紧跟 def) 是,存到 __doc__ 程序/LLM

验证:工具对象的 .description 属性内容就是原始 docstring。装饰器读取 docstring → 存到 .description → 发给 LLM。

所以工具的 docstring 不是写给人看的注释,而是「教 LLM 什么时候该用这个工具」的说明书。

5.3 工具不一定要用框架的装饰器

核心要求是「变成框架的 Tool 类型」,途径有多种:

方式 场景
装饰器(@tool 本地简单工具
继承 Tool 基类 复杂工具,需要内部状态
from_function 精细控制 schema
MCP 工具 + 加载器 远程工具,adapter 自动转换

MCP 工具可以用任何框架写(不一定是 LangChain),通过 adapter 转成框架的 Tool 类型后也能绑定给 LLM。这体现了 MCP 的价值:工具实现与 Agent 框架解耦。


六、工具选择机制(实验深挖)

这是最有价值的一组实验。目标是验证「LLM 凭什么选某个工具」。

实验设定

一个 agent 有两个工具:

  • 工具 T(被测工具):原本是「查时间」,schema 清晰。
  • 工具 R:检索文档,描述明确「查文档」。

逐步把工具 T 的三件套(name / description / args)改模糊,问一个需要 T 的问题,观察 LLM 是否还调。

实验过程

改动 name description args LLM 调 T?
原始 get_time 查时间... timezone
改 description get_time 处理数据 timezone
改 name process_data 处理数据 timezone
改 args process_data 处理数据 region
注释掉工具 不调,说不知道

三件套全模糊后还调,出乎意料。逐步排查原因。

第一次错误归因:以为靠命名识别

被早期一次不相关的环境问题(localhost 走 IPv6 导致连接失败)误导,一度归因为库版本不兼容,后证明与工具选择无关。

第二次错误归因:以为靠工具 schema 精确匹配

改三件套后仍调,开始查对话历史。

真相一:对话历史是强信号

查持久化存储里的历史 messages,发现三件套模糊时 LLM 调用传的参数是旧参数名,而当前 schema 已改新名。

历史记录:
  用户: 现在几点
  AI:   tool_calls=[(工具名, {旧参数名: ...})]   ← 抄历史参数名

结论:LLM 从对话历史抄了参数名,历史盖过当前 schema

真相二:清历史后仍调——排除法 + 试探

清空存储 + 用全新会话 + 三件套模糊,LLM 仍调,但参数变成当前的新名(证明无历史污染)。此时工具列表只有两个:工具 R(描述明确"查文档",排除)、工具 T("处理数据"宽泛)。问题明确需要外部数据,LLM 用排除法选了唯一可能的 T 试探性调用。

最终结论

LLM 选工具不是精确的 schema 匹配,而是综合决策:

需求驱动(问题需要外部数据)
  + 能力推断(看工具 description 推断能力)
  + 排除法(排除明显不匹配的)
  + 试探(剩下最可能的就试调)
  + 对话历史(强信号,能盖过当前 schema)

要让 LLM 真的不调,描述要「明确不相关」(如"发送邮件"),而不是「模糊宽泛」(如"处理数据"——"数据"范围太广,反而成兜底选项)。

附加观察

问一个能从已有信息推导的衍生问题时(如已知 A 地时间,问 B 地时间),LLM 没调工具,直接换算。说明 LLM 知道「已有数据够用时就不调工具」,对调用时机有判断。


七、会话记忆与持久化

7.1 checkpointer 机制

checkpointer 按会话 ID 存取对话状态。同一会话 ID 的多次调用会自动加载历史 messages,LLM 据此有上下文。

config={"configurable": {"thread_id": session_id}}   # 会话 ID 即记忆钥匙

7.2 持久化存储

记忆可以存在内存(重启丢失)或持久化存储(如 SQLite 文件、Redis):

  • 持久化的好处:重启不丢会话,可多实例共享
  • 持久化的代价:调试时历史会污染实验

7.3 踩坑:同步 checkpointer 不能用于 async agent

同步版 checkpointer(如 SqliteSaver)的 async 方法直接抛 NotImplementedError。而 agent 的 ainvoke 走 async 路径会调它。内存版(MemorySaver)之所以能用是因为同时实现了 sync+async,SQLite 同步版没有。

必须用 async 版(如 AsyncSqliteSaver,基于 aiosqlite)。涉及改动:

  • 构造时不再创建 checkpointer,改为 async setup() 延迟初始化
  • 读写历史的方法改 async(用 aget_tuple / adelete_thread
  • app 启动时 await setup(),关闭时 await close()

7.4 调试要点

重启服务 ≠ 清空状态。只要有持久化,状态跨重启存活。干净实验必须显式清状态:

  • 删存储文件(注意 WAL 模式有 -wal / -shm 附属文件)
  • 或调清空接口
  • 或换新会话 ID

八、RAG 路径与 embedding 触发

8.1 两条工具路径

问实时信息(如时间) → LLM 选实时工具 → 不走 RAG
问知识库内容        → LLM 选检索工具 → 走 RAG → 触发 embedding

检索工具的 docstring 决定何时被选。只有问题匹配描述时 LLM 才选它。

8.2 embedding 调用位置

embedding 服务通常有两个方法:

  • embed_query:检索时把查询向量化
  • embed_documents:入库时把文档向量化

完整 RAG 链路:

检索工具(query)
  → 向量库.as_retriever().invoke(query)
    → embed_query(query)        ← 查询向量化
    → 向量库检索相似文档
  → 返回文档片段给 LLM

要断点 embedding:上传文档触发 embed_documents,或问知识库内容触发 embed_query


九、调试方法

9.1 VSCode 断点调试

配置 .vscode/launch.json

{
  "version": "0.2.0",
  "configurations": [{
    "name": "Debug Agent",
    "type": "debugpy",
    "request": "launch",
    "program": "${workspaceFolder}/入口文件.py",
    "console": "integratedTerminal",
    "justMyCode": true,
    "python": "${workspaceFolder}/.venv/Scripts/python.exe",
    "env": { "PYTHONPATH": "${workspaceFolder}" },
    "cwd": "${workspaceFolder}"
  }]
}
  • 打断点:点行号左侧 gutter 出红点(与 PyCharm 相同)
  • F5 启动,F10 单步,F5 继续
  • 断点停下后用「调试控制台」(Ctrl+Shift+Y)求值表达式,如 result["messages"][type(m).__name__ for m in result["messages"]]

9.2 print 验证工具调用

在工具函数内加 print,能直接确认工具是否被调用、参数是什么。比看日志更直观,是验证「LLM 到底调没调这个工具」的最简手段。

9.3 查 checkpoint 历史

import asyncio, aiosqlite
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver

async def main():
    conn = await aiosqlite.connect('checkpoint.db')
    saver = AsyncSqliteSaver(conn)
    tup = await saver.aget_tuple({'configurable': {'thread_id': 'xxx'}})
    msgs = tup.checkpoint['channel_values']['messages']
    for m in msgs:
        print(type(m).__name__, getattr(m, 'tool_calls', None))
    await conn.close()
asyncio.run(main())

能看到完整的 ReAct 痕迹(HumanMessage → AIMessage(tool_calls) → ToolMessage → AIMessage)。


十、关键认知清单

  1. Agent = 大脑(model)+ 手脚(tools)+ 记忆(checkpointer),三者缺一不可。
  2. LLM 不知道任何实时信息(时间、监控、日志、天气),工具是它感知世界的眼睛和手。没有工具,LLM 再强也只能说"我不知道"。
  3. 工具的 docstring 是给 LLM 看的说明书,不是注释。name + description + args 三件套共同决定 LLM 是否选用。
  4. LLM 选工具不是精确匹配,是需求驱动 + 排除法 + 试探 + 历史的综合决策。描述要「明确不相关」才能阻止调用,「模糊宽泛」反而成兜底。
  5. 对话历史是强信号,能盖过当前工具 schema,甚至抄历史里的参数名。调试必须隔离历史。
  6. 持久化让状态跨重启存活,重启服务 ≠ 清状态。SQLite 是零部署的会话存储选择。
  7. 同步 checkpointer 不能直接用于 async agent,必须用 async 版,否则 aget_tuple 抛 NotImplementedError。
  8. 高层封装底层也是图引擎,工厂函数返回的就是编译后的图。
  9. RAG 和 ReAct 不冲突:ReAct 是运行范式,RAG 是工具能力。
  10. 提示工程的投入决定 Agent 可控性:提示薄则行为易被 tools/历史改变,提示厚则稳定可控。
  11. 调试 Agent 要隔离变量:清历史、换会话、看 checkpoint、print 验证,避免被历史/框架默认行为误导。
0

评论区