LAngChain工具接入

在引入工具调用(Tool Calling)时,我们要面临一个全新的底层逻辑:大模型需要"记忆对话"才能使用工具。

🧠 架构思考:为什么引入工具必须引入"消息历史(Messages)"?

在之前的架构中,我们的 State 里只有 current_codeerror_message。大模型每次只看一眼错误,写出一段代码就结束了。

但是,一旦引入了工具,交互过程就变成了多轮对话

  1. AI 发言 :"我要调用 search_web 工具,搜索词是 'LangGraph API'。"
  2. Tools 节点发言:"这是我搜到的结果:xxxx..."
  3. AI 再次发言 :"哦,原来如此。那我开始写代码了:<code_block>..."

为了让这个多轮对话能够顺畅进行,我们的全局 State 里必须引入一个类似微信聊天记录的字段 。在 LangChain 中,这通常被定义为 messages: Annotated[list, add_messages]


🐍 代码实现:打造具备搜索能力的 Coder

我们将采用**"工具绑定(bind_tools) + 标签提取(XML Parsing)"**的混合模式。

这既保留了工具调用的自由,又保证了我们能精准提取出代码。

Step 1: 定义外部工具 (Tools)

我们先用 @tool 装饰器,把一个普通的 Python 函数包装成 AI 能认识的工具。

复制代码
from langchain_core.tools import tool

# ==========================================
# 1. 定义外部工具 (赋予 AI 机械臂)
# ==========================================
@tool
def search_web(query: str) -> str:
    """当你不确定某个库的最新用法,或者缺乏特定知识时,使用此工具搜索网络。"""
    print(f"    [Tools 节点正在运行]: 正在联网搜索 -> {query}")

    # 这里我们用模拟的数据返回。在真实的扩展中,
    # 你可以把这里换成调用 Tavily API、Bing API,或者是你本地的数据库查询代码。
    if "天气" in query or "weather" in query:
        return "最新的天气 API 用法是: import requests; requests.get('http://api.weather.com/v1/...')"
    return "搜索结果:该库的最新版本结构已发生改变,请使用新的实例化方法..."

# 将工具放入列表中备用
tools = [search_web]
Step 2: 升级 State 与大模型

我们要把工具"绑定"到大模型上,并且在 State 里加入 messages 聊天记录。

复制代码
from typing import TypedDict, Annotated, List, Optional
from langgraph.graph.message import add_messages
from langchain_core.messages import BaseMessage, HumanMessage, ToolMessage

# ==========================================
# 2. 升级 State 结构
# ==========================================
class SkillsCreatorState(TypedDict):
    user_requirement: str

    # 【核心新增】:用于记录 AI 和 Tools 之间的多轮聊天记录
    # add_messages 会自动处理消息的追加和合并
    messages: Annotated[List[BaseMessage], add_messages] 

    current_code: str
    current_test_code: str
    error_message: Optional[str]

# ==========================================
# 3. 绑定工具到大模型
# ==========================================
# 我们不再使用 with_structured_output!
# 而是使用 bind_tools,告诉 Claude:"这里有些工具,你需要时可以随时用。"
coder_llm_with_tools = llm.bind_tools(tools)
Step 3: 重写 Coder 节点 (对话模式)

现在的 Coder 节点不再是强行输出代码了,它是一个真正的对话参与者。

复制代码
import re
from langchain_core.messages import HumanMessage, SystemMessage

def coder_node(state: SkillsCreatorState) -> dict:
    """全面升级的 Coder 节点:融合对话、规划、报错与工具调用"""
    print("\n>>> [节点执行]: 进入 Coder 节点 (思考/调用工具/写代码)")

    user_req = state.get("user_requirement")
    plan = state.get("plan", [])
    error_message = state.get("error_message")
    count = state.get("iteration_count", 0)
    
    # 获取当前的全局聊天记录
    messages = state.get("messages", [])

    # ==========================================
    # 步骤一:组装/更新当前轮次的上下文 (Prompt Engineering)
    # ==========================================
    if not messages:
        # 【场景 A:系统刚启动,这是它的第一眼】
        # 1. 设置系统核心人设和 XML 强制输出规则
        system_instruction = """你是一个高级 Python 工程师。
        你可以使用 search_web 工具查阅最新的库文档或解决未知错误。
        规则:
        1. 如果遇到不确定的 API 或报错,请先调用工具查资料。
        2. 当你准备好交付代码时,必须使用以下 XML 标签严格包裹:
        <code_block>这里写业务代码</code_block>
        <test_block>这里写断言测试代码</test_block>"""
        messages.append(SystemMessage(content=system_instruction))
        
        # 2. 告诉它当前的任务和架构师的计划
        plan_str = "\n".join([f"步骤 {i + 1}: {step}" for i, step in enumerate(plan)])
        initial_human_msg = f"用户的需求是:{user_req}\n\n请按照以下计划执行:\n{plan_str}"
        messages.append(HumanMessage(content=initial_human_msg))
        
    elif error_message:
        # 【场景 B:从 Tester 节点被打回,带有报错信息】
        # 注意:我们直接向现有的 messages 列表里追加一条新消息,像是你在微信里骂它
        print(f"    [Coder] 收到 QA 的报错工单,正在进行第 {count} 次思考...")
        error_prompt = f"刚才测试失败了!这是报错信息:\n{error_message}\n请分析错误。如果不确定原因,可以先用工具查一下,然后再输出修复后的 XML 代码。"
        messages.append(HumanMessage(content=error_prompt))

    # 如果既不是第一次进,也没有 error_message,说明是从 Tools 节点回来的!
    # 此时 messages 里已经包含了工具返回的查找结果,不需要我们额外加提示词。

    # ==========================================
    # 步骤二:调用绑定了工具的大模型
    # ==========================================
    # 注意这里使用的是绑定了工具的 llm 实例,传入的是完整的消息列表
    response = coder_llm_with_tools.invoke(messages)

    # ==========================================
    # 步骤三:解析输出,组装状态补丁
    # ==========================================
    patch = {"messages": [response]} # 无论如何,先把 AI 的回答追加进历史记录
    
    # 核心判断:如果 AI 决定交出代码(即没有调用工具),我们尝试解析 XML
    if not response.tool_calls:
        print("    [Coder] AI 未调用工具,开始解析 XML 代码...")
        ai_text = response.content
        # 使用正则提取代码块
        code_match = re.search(r'<code_block>(.*?)</code_block>', ai_text, re.DOTALL)
        test_match = re.search(r'<test_block>(.*?)</test_block>', ai_text, re.DOTALL)
        # 如果提取成功,就更新 current_code,方便下游的 tester_node 使用
        if code_match and test_match:
            patch["current_code"] = code_match.group(1).strip()
            patch["current_test_code"] = test_match.group(1).strip()
            # 只有成功输出代码才算作一次有效的迭代
            patch["iteration_count"] = count + 1 
            # 清空上一轮的报错状态,准备迎接新一轮的测试
            patch["error_message"] = None 
        else:
            # 防呆设计:如果 AI 没按规矩写 XML,我们伪造一个报错,让它在下一轮改
            print("    [Coder Warning] AI 未按 XML 格式输出代码!")
            patch["error_message"] = "你没有使用 <code_block> 和 <test_block> 标签!请重新输出。"
            patch["iteration_count"] = count + 1

    return patch
Step 4: 编写动态路由器 (Router) 与工具节点 (ToolNode)

这是图结构变迁的精髓:我们要在路由器里判断 AI 刚才交出的到底是"请假条(工具调用)"还是"最终成果(代码)"。

复制代码
# ==========================================
# 4. 路由逻辑:AI 到底要干嘛?
# ==========================================
def route_after_coder(state: SkillsCreatorState) -> str:
    """根据 Coder 刚刚输出的 response,决定图的下一步走向"""
    last_message = state["messages"][-1]
    
    # 1. 检查有没有工具请求。如果有,去执行工具
    if hasattr(last_message, "tool_calls") and last_message.tool_calls:
        return "tools"
    
    # 2. 如果没有工具请求,还要防呆检查:它输出有效代码了吗?
    # (利用我们在 coder_node 里如果解析失败会生成 error_message 的逻辑)
    if state.get("error_message") and "没有使用" in state.get("error_message"):
        return "coder" # 格式错了,直接打回重写,不用去测了
        
    # 3. 一切正常,去沙盒测试!
    return "tester"

# ==========================================
# 5. 工具执行节点 (ToolNode)--->放到工具接入代码2了
# ==========================================


# ==========================================
# 全新的图流转逻辑定义
# ==========================================
# (假设 builder 已经添加了 planner, coder, tester, tools 节点)

# 【核心改变 1】:Coder 出口变成了条件路由
builder.add_conditional_edges(
    "coder", 
    route_after_coder,
    {
        "tools": "tools",     # 去查资料
        "tester": "tester",   # 去跑沙盒测试
        "coder": "coder"      # 格式错误,内循环重写
    }
)

# 【核心改变 2】:Tools 执行完毕后,必须无条件回到 Coder!
builder.add_edge("tools", "coder")

💡 核心机制剖析

仔细观察这段设计,你会发现它精妙地解决了我们之前讨论的矛盾:

  • 动态解耦 :我们在 coder_node 里只负责调用 llm.invoke根本不在这里提取代码 。至于下一步去哪里,全权交给了路由器 route_after_coder 来判断。
  • 双向奔赴的循环(Coder \\leftrightarrows Tools)

如果 Coder 调用了工具,路由器把它发给 tools_node

tools_node 执行完搜索,生成了 ToolMessage 存入 State。

然后,流转必须回到 Coder! Coder 再次醒来,看到 messages 里多了一条带有搜索结果的 ToolMessage,它说:"太好了,我知道怎么写了。" 于是它输出了带有 <code_block> 的文本,这一次,路由器就会把它送往 tester_node

  • XML 解析 :在进入 tester_node 之前(或者在 tester_node 内部的第一步),我们只需要用简单的正则表达式 re.search(r'<code_block>(.*?)</code_block>', text) 把代码挖出来塞给沙盒就可以了,这就彻底摆脱了 Pydantic 的格式报错束缚。
相关推荐
怕浪猫2 小时前
第12章 工具(Tools)与函数调用(LangChain实战)
langchain·aigc·ai编程
老王熬夜敲代码2 小时前
接入工具代码讲解
langchain
是小蟹呀^12 小时前
【总结】LangChain中工具的使用
python·langchain·agent·tool
AI应用实战 | RE15 小时前
004、语言模型接口实战:OpenAI、本地模型与流式响应的那些坑
langchain
AI应用实战 | RE16 小时前
012、检索器(Retrievers)核心:从向量库中智能查找信息
人工智能·算法·机器学习·langchain
海兰16 小时前
【第2篇】LangChain的初步实践
人工智能·langchain
AI应用实战 | RE20 小时前
014、索引高级实战:当单一向量库不够用的时候
数据库·人工智能·langchain
是小蟹呀^1 天前
【总结】LangChain中如何维持记忆
python·langchain·memory
Rick19931 天前
LangChain和spring ai是什么关系?
人工智能·spring·langchain