news 2026/6/21 1:48:11

接入工具代码讲解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
接入工具代码讲解

❓ 疑问一: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",把流程拐进工具节点!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/21 1:46:39

抖音广告批量制作神器:用MATLAB+FFmpeg实现自动视频拼接(附完整源码)

基于MATLAB与FFmpeg的智能视频合成系统开发指南 短视频内容创作已进入工业化生产时代&#xff0c;一个成熟的营销团队每周需要处理数百条视频素材的剪辑与组合。传统手动操作不仅效率低下&#xff0c;还容易因人为因素导致输出质量不稳定。本文将完整呈现一套基于MATLAB App De…

作者头像 李华
网站建设 2026/5/28 3:21:36

多模型场景下的成本治理指标体系幢

为 HagiCode 添加 GitHub Pages 自动部署支持 本项目早期代号为 PCode&#xff0c;现已正式更名为 HagiCode。本文记录了如何为项目引入自动化静态站点部署能力&#xff0c;让内容发布像喝水一样简单。 背景/引言 在 HagiCode 的开发过程中&#xff0c;我们遇到了一个很现实的问…

作者头像 李华
网站建设 2026/4/13 22:54:29

运动声源的到达结构仿真

概要 运动声源的到达结构仿真中&#xff0c;由于声传播速度远高于声源运动速度&#xff0c;而且声源辐射出声波后&#xff0c;介质的振子传递声波几乎不受声源影响&#xff0c;因此可以将根据每个时间帧的声源位置&#xff0c;使用bellhop计算到达结构&#xff0c;数字离散采样…

作者头像 李华
网站建设 2026/4/13 22:54:28

企业官网设计那个最好?怎么才能融入品牌文化的视觉设计与前端落地

企业官网设计&#xff1a;如何通过视觉设计与前端技术深度融入品牌文化 企业官网设计不仅是信息窗口&#xff0c;更是品牌文化的立体化载体。优秀的官网设计需实现美学表达、用户体验与品牌内核的三维统一&#xff0c;本文将系统解析设计策略与落地路径。 推荐选择https://ww…

作者头像 李华