利用 Tool-Calling 增強 LangGraph

  1. Tool-Calling 原理 ==================

上一節的內容中,我們藉助 LangGraph 框架、基於 DeepSeek 模型的驅動、通過多個節點的協作,快速搭建起了一個智能編輯 Agent,並且幫我們生成了一篇完整的文章,整體流程還是比較清晰的。

但是,不知道大家有沒有發現這樣一個問題:Agent 實際生成文章的內容是比較陳舊的,有時可能甚至是一兩年以前的知識,無法滿足讀者消費熱點內容的需求。這是因爲我們並沒有開啓聯網搜索功能,還是依賴大模型的預訓練數據,而由於預訓練所要消耗的成本高昂,大模型通常要幾個月才能訓練一次,這就導致了我們文章的內容可能是過時的!想要解決這個問題也很簡單——只需通過 Tool-Calling 功能,開發一個聯網搜索的工具即可。

Tool-Calling ,也叫做 Function-Calling,它是經典的擴展大模型能力邊界的方式,它的核心執行流程如下:

  1. 在調用 LLM 生成內容時,額外傳入一些工具或者函數的定義,每個工具都可以完成一項或多項特定的功能。

  2. 當大模型在生成回覆時,如果推理出需要調用特定的工具,則會在消息中返回工具調用的名稱即調用參數。

  3. 由業務應用解析調用參數,定位到需要調用的工具,實際執行調用。

  4. 業務應用將工具調用結果,作爲下一條消息,繼續發送給 LLM。

  5. LLM 結合上下文,以及工具調用結果,生成最終的完整回覆。

需要特別注意的是:LLM 的返回的只是需要調用的工具、以及工具調用的具體參數,而實際調用工具的行爲還是需要業務應用自行來完成,無法靠 LLM 來代勞。這裏的工具,可以是泛指廣義上的擴展能力,它可以是一個本地函數、一個第三方 API、也可以是一個代碼沙箱運行環境、或者一個數據庫等等。

  1. 開發 Search Tool =================

理解了 Tool-Calling 的核心原理後,我們把它實際落地到我們的 Agent 中。在現有的智能編輯 Agent 流程下,我們擴展一個新功能:在撰寫正文時,先根據文章的主題,到搜索引擎去檢索相關的內容,並依據這些檢索結果再去生成正文,這樣可以大幅提升內容的時效性與權威性。

要實現這個功能,首先我們需要開發一個網絡搜索工具。這裏我們直接採用 **Serper API:**它是一個基於 Google Search 的簡單易用的搜索 API,在 Google 的搜索能力上進行了低延遲和易用性的封裝,可以基於關鍵詞快速的完成網絡檢索功能。Serper API 的使用非常簡單,大家自行查閱官方文檔即可。

爲了實現搜索功能,我們首先 Serper API 封裝成一個本地函數,基於 query 關鍵詞檢索相關結果。

接下來,我們要把它封裝成一個 LLM 可以理解的工具定義。在 LangGraph 下,定義一個工具非常簡單,可以優先去內置的工具箱中查看是否有現成的工具,也可以基於 @tool 裝飾器,直接將一個本地函數封裝成一個 Tool。這裏我們採用 @tool 裝飾器的方式,函數的註釋會被自動解析爲工具和參數的描述信息。注意:這裏的 api_key 需要到 Serper API 官網創建。

# 定義檢索工具
@tool
def search(query: str) -> str:
    """根據關鍵詞,在互聯網上檢索相關信息
    Args:
        query: 檢索關鍵詞
    """
    print(f"調用工具: [search], 調用參數: {query}")
    # 構造請求參數
    url = "https://google.serper.dev/search"
    payload = json.dumps({
        "q": query,
    })
    headers = {
        "X-API-KEY": os.getenv("SERPER_API_KEY"),  # 在Serper官網申請api_key
        'Content-Type''application/json'
    }
    # 發送請求
    response = requests.request("POST", url, headers=headers, data=payload).json()
    # 解析響應結果
    if response['organic'][0]:
        return response['organic'][0]['snippet']
    return "沒有搜索到相關結果"

這樣,一個網絡搜索工具就完成了!

  1. Content Node 邏輯改造 ====================

Search Tool 實現完成之後,我們需要對現有的 Content Node 的邏輯進行改造。第一步就是要調整 System Prompt,引導 LLM 先去根據文章主題,去搜索引擎檢索相關信息:

# 正文Prompt
CONTENT_PROMPT = """
文章的標題爲:《{title}》
請先根據文章主題,在互聯網上檢索相關資料。
接下來,根據資料的檢索結果,按照總-分-總的結構,生成文章的主要段落。段落數在3~6段之間,可以結合你的經驗和主題靈活調整。
下面是一個可供參考的段落結構:
1. xxx綜述
2. xxx的歷史演進
3. xxx的核心概念
4. xxx實戰
5. xxx總結
格式要求:生成的內容純文本格式,每一段都要有一個二級標題(##)。段落之間保留2行空行。
"""

第二步,我們定義一個工具列表,並將大模型與工具進行綁定。這一步可以直接通過 LangChain 內置的  bind_tools 函數來完成:

# 定義工具列表
TOOLS = [search]
TOOL_DICT = {tool.name: tool for tool in TOOLS}
# 將DeepSeek綁定工具列表
deepseek = deepseek.bind_tools(TOOLS)

最後一步,調用 LLM 的流程也要進行修改:我們需要先判斷 LLM 的回覆是否包含工具調用信息,如果包含,則需要解析工具調用參數,並執行工具調用,還有將工具調用結果封裝成一條 ToolMessage , 繼續傳遞給 LLM。

最終的完整實現如下:

def content_node(state: ArticleState) -> ArticleState:
    """
    文章正文內容節點
    :param state: 當前文章狀態
    :return: 處理後的文章狀態
    """
    # 從當前狀態中獲取所需信息
    if not state["topic"]:
        raise ValueError("未指定文章主題!")
    if not state["title"]:
        raise ValueError("文章標題缺失!")
    topic = state["topic"]
    title = state["title"]
    # 構造消息列表
    messages = [
        SystemMessage(content=SYSTEM_PROMPT.format(topic=topic)),
        HumanMessage(content=CONTENT_PROMPT.format(title=title)),
    ]
    # 獲取DeepSeek客戶端
    deepseek = get_deepseek()
    # 將DeepSeek綁定工具列表
    deepseek = deepseek.bind_tools(TOOLS)
    # 調用DeepSeek,獲取結果
    while True:
        # 調用LLM,並保存結果
        reply = deepseek.invoke(messages)
        messages.append(reply)
        # 未返回工具調用參數,說明模型直接生成了回覆,則更新狀態後返回
        if len(reply.tool_calls) == 0:
            content = reply.content
            state["content"] = content
            print(f"文章正文生成完成:共 {len(content)} 字")
            return state
        # 解析工具調用參數
        for tool_call in reply.tool_calls:
            # 根據名稱,獲取需要調用的工具
            tool_name = tool_call["name"]
            tool = TOOL_DICT[tool_name]
            if tool is not None:
                # 調用工具,獲取結果
                tool_call_result = tool.invoke(tool_call["args"])
                print(f"工具調用結果: {tool_call_result}")
                # 保存工具調用結果
                tool_msg = ToolMessage(content=tool_call_result, tool_call_id=tool_call["id"])
                messages.append(tool_msg)
  1. Agent 節點編排 =============

Content Node 改造好了之後,核心的功能就實現完成了,其它 Node 的功能保持不變。

這裏也可以充分體現出 LangGraph 的一大優勢:將整個 Agent 的實現流程拆分到不同的 Node 中去完成,每個 Node 可以獨立變更和擴展,不會對整體流程造成影響,這樣就大幅提升了 Agent 系統的可擴展性和魯棒性。

最終編排 Agent 並執行的代碼如下:

def build_agent() -> CompiledStateGraph:
    """
    構造Agent
    :return: 編譯好的Agent Graph
    """
    # 創建StateGraph圖結構
    sg = StateGraph(ArticleState)
    # 添加節點
    sg.add_node("title_node", title_node)
    sg.add_node("content_node", content_node)
    sg.add_node("summary_node", summary_node)
    sg.add_node("image_node", image_node)
    # 添加邊
    sg.add_edge(START, "title_node")
    sg.add_edge("title_node""content_node")
    sg.add_edge("content_node""summary_node")
    sg.add_edge("summary_node""image_node")
    sg.add_edge("image_node", END)
    # 編譯Graph並返回
    graph = sg.compile() 
    return graph
def write_article(agent: CompiledStateGraph, topic: str) -> ArticleState:
    """
    撰寫文章
    :param agent: 內容編輯Agent
    :param topic: 文章主題
    :return: 最終生成的文章狀態
    """
    # 設置初始狀態
    init_state = ArticleState(
        topic=topic,
        title="",
        content="",
        summary="",
        image_path="",
    )
    # 執行Graph,返回生成的最終狀態
    return agent.invoke(init_state)
def dump_markdown(state: ArticleState) -> None:
    """
    將文章以Markdown格式保存到本地
    :param state: 最終狀態
    :return: None
    """
    title = state["title"]
    with open(f"./{title}.md""w") as f:
        # 寫入標題
        f.write(f"# {state["title"]}\n\n")
        # 寫入正文
        f.write(f"{state["content"]}\n\n")
        # 在末尾插入圖片
        f.write(f"![{title}]({state["image_path"]})\n\n")
    print(f"文章已保存至:{title}.md")
def show_agent_structure(agent: CompiledStateGraph) -> None:
    """展示Agent 結構"""
    display(Image(agent.get_graph(xray=True).draw_mermaid_png()))
    # 保存流程圖到文件
    graph_png = agent.get_graph(xray=True).draw_mermaid_png()
    with open("agent_graph.png""wb") as f:
        f.write(graph_png)
if __name__ == '__main__':
    # 構造Agent
    agent = build_agent()
    # 展示Agent結構圖
    # show_agent_structure(agent)
    # 寫文章
    final_state = write_article(agent=agent, topic="中國蘇超聯賽持續火爆")
    # 將文章導出爲markdown格式
    dump_markdown(final_state)

這裏我新增了一個函數 show_agent_structure:可以直觀地打印出整個 Graph 的結構,展示結果如下:

  1. 效果展示 =======

我們指定一個近期的熱點話題——中國蘇超聯賽,讓編輯 Agent 圍繞這個話題,去檢索相關資料,並幫我們生成一篇文章。可以查看日誌中的處理流程如下:

最終生成的文章內容爲:

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/bEANFpntbpSIDe_9ORAxWw