接入工具代码讲解


❓ 疑问一:AI 是一定会调用工具,还是会直接生成代码?

答案是:全看 AI 自己觉得需不需要!它拥有完全的"自主决定权"。

在传统的编程里,代码写了 if 就走分支,写了 requests.get 就一定发请求。但在 Agent 的世界里,coder_llm_with_tools.invoke(messages) 这一行代码,就像是把一份**"开卷考试的考卷"和一部"能上网的手机(工具)"**同时放在了 Claude 面前。

  • 场景 A:闭眼作答(直接生成代码)

假设用户的需求是:"写一个计算斐波那契数列的函数"。

Claude 看到这个需求,心里想:"这太简单了,我闭着眼睛都能写,根本不需要查资料。"

于是,它不会输出任何工具调用指令,而是直接老老实实地输出一段带有 **<code_block>**的纯文本

我们的路由器(Router)看到它没用工具,就会直接把它送去 Tester 节点。

  • 场景 B:翻书作答(调用工具)

假设用户的需求是:"使用最新的 LangGraph 0.3 版本 API 写一个状态图"。

Claude 看到后,心里一惊:"我的训练数据只到 2024 年,我不知道 0.3 版本长什么样。"

这时候,它就会主动选择**"求助"**。它会输出一个 ToolCall****指令,要求使用 search_web****工具去搜"LangGraph 0.3 API"。

路由器发现它求助了,就会把它送到 Tools 节点去查资料。

💡 总结: 我们通过 bind_tools 只是给了它"查资料的能力和权利",但绝对不强制它必须查。这就是所谓"智能"的体现------它会根据自己掌握知识的边界,动态决定要不要使用工具。


❓ 疑问二:正则表达式(Regex)到底是怎么提取代码的?

正则表达式(Regular Expression,简称 Regex)就像是给文本做的一台"高精度外科手术"。大模型往往很啰嗦,可能会说:"好的老板,这是您要的代码:\n <code_block>def test(): pass</code_block> \n 希望您满意!"

我们需要把废话剔除,只精准地把尖括号中间的代码"抠"出来。

让我们彻底解剖这行代码:

code_match = re.search(r'<code_block>(.*?)</code_block>', ai_text, re.DOTALL)

1. 拆解匹配规则 r'<code_block>(.*?)</code_block>'
  • r'...':前面的 r 代表"原始字符串"(Raw string),意思是告诉 Python 忽略转义字符,里面写啥就是啥。
  • **<code_block>** </code_block>:这就像是两个"定位锚点"。告诉正则引擎:"你先给我找到左边这个词,再找到右边这个词。"
  • (.?)***(这是最核心的魔法)
    • .****(点):代表**"匹配任何单个字符"**(字母、数字、标点都可以)。
    • *****(星号) :代表**"重复前面的字符 0 次或无数次"**。所以 .* 连起来就是"匹配中间的任意一长串内容"。
    • ?****(问号,非贪婪模式) :如果没有这个问号,正则会非常"贪婪",它会从第一个 <code_block> 一直匹配到整篇文章最后一个 </code_block>(如果文中有好几个块就全乱套了)。加了 ? 就是告诉它:"只要遇到第一个 </code_block>,你就赶紧停下来!"
    • ()****(括号,捕获组) :圆括号的意思是"提取"。虽然我们匹配了整个 <code_block>...内容...</code_block>,但我只想要括号里面的"内容",不想要两边的标签。这叫捕获组 1
2. 为什么要加 re.DOTALL 标志?

这是一个极其容易踩坑的地方!

在正则表达式的默认规则里,刚才说的那个神奇的 .(点),唯独不能匹配一种东西:换行符( \n**)**。

但是,我们提取的 Python 代码通常是有十几行的,中间全是换行符!如果不加这个参数,匹配会在第一行结束时直接断掉报错。

加上 re.DOTALL,就是赋予了 . 特权:"从现在起,连换行符你也给我统统匹配进去!"

3. 如何拿到提取的结果?
复制代码
if code_match:
    # group(0) 会返回完整的:<code_block>def a(): pass</code_block>
    # group(1) 只会返回括号里捕获的纯净代码:def a(): pass
    pure_code = code_match.group(1).strip() # strip() 用于去首尾的空格和空行

❓ 疑问三:last_message到底是个什么对象?

当代码执行完 response = coder_llm_with_tools.invoke(messages) 时,返回的 response(也就是我们存进状态里的 last_message),并不是一个普通的字符串,而是 LangChain 框架定义的一个对象(Object) ,叫做 AIMessage

这个 AIMessage 对象身上,有两个极其重要的属性(字段):

  1. content:用来存放 AI 说的普通文本内容(比如写好的代码、或者聊天的废话)。
  2. tool_calls:这是一个列表(List) ,专门用来存放 AI 发出的工具调用请求

🎬 场景还原:两种情况下的内存快照

让我们看看在你刚才提问的两种情况下,这个对象在内存里长什么样。

情况 A:AI 决定直接写代码(不查资料)

当 AI 觉得题目太简单,直接输出带 <code_block> 的代码时。

这个 AIMessage 对象的内部结构是这样的:

复制代码
AIMessage(
    content="好的,这是代码:\n<code_block>def test(): pass</code_block>",
    tool_calls=[],  # <--- 注意看这里!列表是空的!
    # ... 其他属性忽略
)

此时,路由器执行到 if last_message.tool_calls:

在 Python 中,一个空的列表 **[]**会被当成 False。所以 if 条件不成立,代码不会进入 Tools 节点,而是顺理成章地走向了 Tester 节点。

情况 B:AI 决定求助(调用 search_web

当 AI 遇到"LangGraph 0.3 API"这种知识盲区时,由于我们用 bind_tools 提前告诉过它"你有工具可用",它就会在底层触发一个特殊的生成模式。

这时候,生成的 AIMessage 对象结构变了:

复制代码
AIMessage(
    content="",  # <--- 注意!通常文本内容是空的,因为它没在说话,而在按按钮。
    tool_calls=[ # <--- 核心魔法在这里!列表里多了一个字典!
        {
            "name": "search_web",                  # 它想要调用的工具名称
            "args": {"query": "LangGraph 0.3 API"}, # 它自动提取的搜索关键字
            "id": "call_abc123xyz"                 # 这次调用的唯一流水号
        }
    ],
    # ... 其他属性忽略
)

此时,路由器再次执行到 if last_message.tool_calls:

这次,列表里有东西了!在 Python 中,非空列表会被当成 True

于是 if 条件成立!程序立刻打印 [Router] 决策:Coder 需要查资料...,并把流程导向了 Tools 节点。


💡 为什么需要那个特殊的 id 字段?(进阶思考)

你可能会注意到上面那个 call_abc123xyz。这在微服务通信里叫"请求溯源 ID"。

当我们的 Tools 节点去百度搜完结果后,我们要把结果发回给大模型。大模型怎么知道这个结果对应的是它哪一次的提问呢?

所以在 Tools 节点里,我们必须把这个 ID 抄下来,连同搜索结果一起打包成一个 ToolMessage 发给它:

复制代码
ToolMessage(
    content="搜索结果:LangGraph 0.3 引入了新的...", 
    tool_call_id="call_abc123xyz" # <--- 拿着单号去交差
)

大模型一看到这个单号对上了,就会恍然大悟:"哦!这是我刚才查的资料结果回来了!我现在可以开始写代码了!"


🔬 代码逻辑深度拆解

在这个十字路口,路由器拿到了 last_message(也就是 AI 刚刚在 coder_node 里吐出来的那个对象)。

第一道关卡:hasattr(last_message, "tool_calls")
  • 这是什么? 这是 Python 中的内置函数,意思是:"查一下 last_message 这个对象身上,有没有一个叫做 tool_calls 的属性(字段)?"
  • 为什么要这么写(防御性编程)? 在 LangChain 的世界里,消息有很多种类型:HumanMessage(人类说的)、SystemMessage(系统设定的)、以及 AIMessage(AI 说的)。

通常情况下,last_message 肯定是个 AIMessage。但作为严谨的工程师,我们要防患于未然:万一系统出了 Bug,传过来一个根本没有 tool_calls 属性的奇怪对象,如果你直接写 if last_message.tool_calls:,Python 解释器会立刻原地爆炸,抛出 AttributeError 导致整个微服务宕机。

所以,先礼后兵。先问一句:"兄弟,你身上有这个口袋吗?"

第二道关卡:and last_message.tool_calls
  • 这是什么? 只有当第一道关卡通过(它确实有这个口袋)时,程序才会执行这半句。这半句的意思是:"既然你有这个口袋,你把口袋翻开,里面有东西吗?"
  • 底层逻辑

就像我们上节课"抓包"看到的:

    • 如果 AI 决定直接写代码,这个口袋是个空列表 []。在 Python 的 if 语句中,空列表等同于 False。条件不成立,继续往下走(去测代码)。
    • 如果 AI 决定用 search_web,这个口袋里就会有一个字典 [{'name': 'search_web', ...}]非空列表等同于 True。条件成立,立刻返回 "tools",把流程拐进工具节点!

相关推荐
是小蟹呀^12 小时前
【总结】LangChain中工具的使用
python·langchain·agent·tool
AI应用实战 | RE15 小时前
004、语言模型接口实战:OpenAI、本地模型与流式响应的那些坑
langchain
AI应用实战 | RE15 小时前
012、检索器(Retrievers)核心:从向量库中智能查找信息
人工智能·算法·机器学习·langchain
海兰16 小时前
【第2篇】LangChain的初步实践
人工智能·langchain
AI应用实战 | RE19 小时前
014、索引高级实战:当单一向量库不够用的时候
数据库·人工智能·langchain
是小蟹呀^21 小时前
【总结】LangChain中如何维持记忆
python·langchain·memory
Rick19931 天前
LangChain和spring ai是什么关系?
人工智能·spring·langchain
AI应用实战 | RE1 天前
011、向量数据库入门:Embeddings原理与ChromaDB实战
开发语言·数据库·langchain·php
Rick19931 天前
LangChain 核心解析:底层架构、原理
架构·langchain