news 2026/9/12 21:13:14

LangChain02 Agent核心双翼

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LangChain02 Agent核心双翼

LangChain02 Agent核心双翼

核心目标:掌握Tool Calling(工具调用)RAG(检索增强生成)。这是让大模型“长出手脚”和“拥有外部大脑”的绝对核心,也是后续学习 LangGraph 的底层基石。
核心心法:理解底层闭环逻辑,拒绝黑盒,手写循环,拥抱 LCEL。


一、Tool Calling:让模型“长出手脚”

核心认知:大模型不会直接执行 Python 代码。它只负责“决定”调用哪个工具并提取参数,真正的执行由后端代码完成。

1. 工具的三要素

使用@tool装饰器定义工具,模型主要依靠以下三要素来决定是否调用:

  • name:工具名称。
  • description (docstring):工具能力与适用场景(写得越清晰,调用越准)。
  • args schema:参数类型与说明。

2. 核心代码骨架

fromlangchain_core.toolsimporttoolfromlangchain_core.messagesimportHumanMessage,ToolMessage# 1. 定义工具@tooldefget_weather(city:str)->str:"""查询指定城市的天气。当用户询问天气时使用。"""returnf"{city}今天晴天,26度。"tools=[get_weather]# 2. 绑定工具给模型llm_with_tools=llm.bind_tools(tools)# 3. 调用模型并检查 tool_calls (核心!)response=llm_with_tools.invoke([HumanMessage(content="北京天气如何?")])# 判断模型是否决定调用工具ifresponse.tool_calls:fortool_callinresponse.tool_calls:tool_name=tool_call["name"]tool_args=tool_call["args"]tool_id=tool_call["id"]# 4. 后端执行工具result=get_weather.invoke(tool_args)# 5. 包装为 ToolMessage 并喂回模型 (必须带 tool_call_id)tool_msg=ToolMessage(content=result,tool_call_id=tool_id)# 6. 再次调用模型生成最终自然语言回答final_response=llm_with_tools.invoke([HumanMessage(...),response,tool_msg])

3. 手写 Agent 闭环 (LangGraph ToolNode 的底层原理)

用户提问 -> LLM 判断 -> 有 tool_calls? ├── Yes -> 执行 Python 函数 -> 构造 ToolMessage -> 追加到 messages -> 回到 LLM └── No -> 输出最终回答 (response.content)

4.最终代码

importsysfromlangchain_core.messagesimportHumanMessage,ToolMessagefrompathlibimportPath# 将项目根目录加入 sys.path,以便导入 day01 目录中的模块sys.path.insert(0,str(Path(__file__).resolve().parent.parent))fromlangchain_core.toolsimporttoolfromday01.day01importllm@tooldefget_weather(city:str)->str:""" 查询指定城市的天气。 当用户询问某个城市天气时使用。 参数: city: 城市名称,例如“北京”、“上海”。 """weather_data={"北京":"晴天,气温 26 度","上海":"多云,气温 24 度","广州":"小雨,气温 28 度","焦作":"晴天,气温30度","临沂":"多云,气温20度"}returnweather_data.get(city,f"暂时无法查询{city}的天气")@tooldefcalculate_math(expression:str)->str:""" 计算数学表达式。 当用户需要进行数学计算时使用。 参数: expression: 合法的数学表达式,例如 "2 + 3 * 4"。 """try:# 学习演示用,生产环境不要直接用 evalresult=eval(expression,{"__builtins__":{}},{})returnstr(result)exceptExceptionase:returnf"计算失败:{e}"# # 主程序入口# if __name__ == '__main__':""" 1. 用户提问 2. 模型判断需要工具 3. 模型返回 tool_calls 4. 代码根据 tool name 找到对应工具 5. 代码执行工具,得到结果 6. 把结果包装成 ToolMessage 7. 把 ToolMessage 加回 messages 8. 再次调用模型 9. 模型根据工具结果生成最终回答 """tools=[get_weather,calculate_math]# 2. 建立工具名到工具对象的映射tools_by_name={t.name:tfortintools}# 3. 绑定工具给模型llm_with_tools=llm.bind_tools(tools)# 4. 手写 Agent 循环defrun_agent(question:str,max_steps:int=5)->str:messages=[HumanMessage(content=question)]forstepinrange(max_steps):print(f"\n=== 第{step+1}步:调用模型 ===")ai_message=llm_with_tools.invoke(messages)messages.append(ai_message)print("模型返回 content:",ai_message.content)print("模型返回 tool_calls:",ai_message.tool_calls)# 如果没有工具调用,说明模型已经给出最终回答ifnotai_message.tool_calls:returnai_message.content# 如果有工具调用,就执行工具fortool_callinai_message.tool_calls:tool_name=tool_call["name"]tool_args=tool_call["args"]tool_call_id=tool_call["id"]print(f"模型决定调用工具:{tool_name}")print(f"工具参数:{tool_args}")selected_tool=tools_by_name[tool_name]tool_result=selected_tool.invoke(tool_args)print(f"工具执行结果:{tool_result}")# 把工具结果包装成 ToolMessagemessages.append(ToolMessage(content=str(tool_result),tool_call_id=tool_call_id))print("最新的消息为:{messages}")return"达到最大执行步数,任务未完成。"# 5. 测试answer=run_agent("焦作今天天气怎么样?")print("\n最终回答:",answer)

二、RAG:让模型“拥有外部大脑”

核心认知:将私有知识向量化存储,用户提问时检索最相关的片段(Top-K),塞入 Prompt 让模型基于上下文回答,从而解决幻觉和知识滞后问题。

1. RAG 核心流程与组件

步骤核心组件 / 方法关键参数 / 作用
1. 加载原生open()+Document替代废弃的TextLoader,避免编码警告。
2. 切分RecursiveCharacterTextSplitterchunk_size(块大小),chunk_overlap(重叠防语义断裂)。
3. 向量化Embeddings(如 DashScope/OpenAI)将文本转为高维向量。
4. 存储FAISS/Chroma本地轻量级向量数据库。
5. 检索vectorstore.as_retriever()search_kwargs={"k": 3}(返回最相关的3个块)。

2. 核心难点:RunnableLambda格式化

检索器返回的是[Document, Document]对象列表,必须用RunnableLambda将其转为带编号的字符串,以便省 Token 且支持引用。

fromlangchain_core.runnablesimportRunnableLambdadefformat_docs(docs):return"\n\n".join(f"[{i}]{doc.page_content}"fori,docinenumerate(docs,1))format_runnable=RunnableLambda(format_docs)

3. LCEL 组装 RAG Chain

使用字典和RunnablePassthrough并行传递contextquestion

fromlangchain_core.runnablesimportRunnablePassthrough rag_chain=({"context":retriever|format_runnable,# 检索并格式化"question":RunnablePassthrough()# 原样传递用户问题}|rag_prompt# 包含 {context} 和 {question} 的 Prompt|llm)answer=rag_chain.invoke("学生请假流程是什么?")

4. 防幻觉 Prompt 设计 (必加)

你必须只根据上下文回答问题。 如果上下文中没有答案,请回答:根据上下文无法确定。 不要编造信息。回答时请使用引用编号,例如 [1]、[2]。

5.RAG最终代码

importosfromdotenvimportload_dotenvfromlangchain_core.documentsimportDocumentfromlangchain_openaiimportChatOpenAI# from langchain_community.document_loaders import TextLoaderfromlangchain_text_splittersimportRecursiveCharacterTextSplitterfromlangchain_community.embeddingsimportDashScopeEmbeddingsfromlangchain_community.vectorstoresimportFAISSfromlangchain_core.promptsimportChatPromptTemplatefromlangchain_core.runnablesimportRunnableLambda,RunnablePassthrough load_dotenv()# 1. 初始化 LLMllm=ChatOpenAI(model="kimi-k2.7-code",base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",api_key=os.getenv("DASHSCOPE_API_KEY"),temperature=1,)# 2. 初始化 Embeddingsembeddings=DashScopeEmbeddings(model=os.getenv("embedding_name"),dashscope_api_key=os.getenv("DASHSCOPE_API_KEY"))# 3. 加载文档# (1) 用 Python 原生方式读取文件file_path="handbook.txt"withopen(file_path,"r",encoding="utf-8")asf:text=f.read()# (2) 手动包装成 LangChain 的 Document 对象documents=[Document(page_content=text,metadata={"source":file_path}# metadata 用于记录来源,RAG 中很有用)]# 4. 切分文档splitter=RecursiveCharacterTextSplitter(chunk_size=300,chunk_overlap=60)chunks=splitter.split_documents(documents)print(f"切分文档后chunks的类型是{type(chunks)}")# 5. 存入向量数据库vectorstore=FAISS.from_documents(documents=chunks,embedding=embeddings)# 6. 创建检索器retriever=vectorstore.as_retriever(search_type="similarity",search_kwargs={"k":3})# 7. 格式化检索结果defformat_docs(docs):return"\n\n".join(f"[{i}]{doc.page_content.strip()}"fori,docinenumerate(docs,1))# 8. 定义 Promptrag_prompt=ChatPromptTemplate.from_messages([("system",""" 你是一个本地文档问答助手。 你必须只根据上下文回答问题。 如果上下文中没有答案,请回答:根据上下文无法确定。 不要编造信息。 回答时请使用引用编号,例如 [1]、[2]。 上下文: {context} """),("human","{question}")])# 9. 构建 RAG Chainrag_chain=({"context":retriever|RunnableLambda(format_docs),"question":RunnablePassthrough()}|rag_prompt|llm)# 10. 测试question="硕士1班有什么学生,学号是多少"response=rag_chain.invoke(question)print(response.content)

三、避坑指南 (Day 2 Don’ts)

  • 不要看response.content来判断工具调用:必须看response.tool_calls
  • 不要漏掉tool_call_idToolMessage必须带上 ID,否则模型不知道结果对应哪次调用。
  • 不要用老版TextLoader:会报 DeprecationWarning,直接用 Pythonopen()+Document
  • 不要学老版RetrievalQA:直接用 LCEL 自己拼装 RAG Chain。
  • 不要碰复杂的 Memory 类:如ConversationBufferMemory,LangGraph 中用messages状态和Checkpointer管理记忆。
  • 不要一上来学高级 RAG:先跑通基础 Top-K 检索,不要碰 Parent Document、Hybrid Search 等。

四、面试Q&A

Q1:Function Calling 的本质是什么?模型真的执行代码了吗?

A:模型没有执行代码。本质是模型根据用户输入和工具的 description/schema,生成一个结构化的 JSON 请求(包含工具名、参数和 ID)。后端代码解析这个请求,执行真正的 Python 函数,然后将结果通过ToolMessage喂回给模型,模型再基于结果生成自然语言回答。

Q2:为什么 RAG 中需要chunk_overlap(文本重叠)?

A:因为机械切分文本可能会把完整的一句话或一个逻辑段落从中间切断,导致语义丢失。设置chunk_overlap让相邻的文本块保留一部分重叠内容,可以保证上下文语义的连贯性,提高检索准确率。

Q3:在 LCEL 中构建 RAG 时,为什么要用RunnableLambda

A:因为 Retriever 检索出来的是Document对象列表,而 Prompt 需要的是纯文本字符串。RunnableLambda允许我们将自定义的 Python 格式化函数(如拼接带[1] [2]编号的字符串)包装成 LCEL 兼容的组件,无缝接入管道|中。

Q4:手写的 Tool Calling while 循环和 LangGraph 有什么关系?

A:手写的 while 循环就是 LangGraph 中 Agent 节点和 Tool 节点交互的底层逻辑。在 LangGraph 中,这个循环被抽象成了状态图:Agent 节点判断是否有tool_calls,如果有,通过条件边(Conditional Edge)路由到 Tool 节点执行,执行完后再路由回 Agent 节点,直到没有工具调用为止。


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

Flow3D 11.1在SLM金属3D打印工艺模拟中的应用

1. Flow3D 11.1与SLM工艺模拟概述 金属3D打印领域的选区激光熔化(SLM)技术,正在彻底改变传统制造业的生产方式。而Flow3D 11.1作为专业的计算流体动力学软件,其强大的多物理场耦合能力为SLM工艺开发提供了全新的数字化工具组合。不…

作者头像 李华
网站建设 2026/9/12 21:10:51

ToolJet List View 组件完全指南:数据列表、分页与子组件控制

ToolJet List View 组件完全指南:数据列表、分页与子组件控制 【免费下载链接】ToolJet Open-source foundation of ToolJet AI - the enterprise app generation platform for internal tools, dashboards, business applications, workflows and AI agents. Build…

作者头像 李华
网站建设 2026/9/12 21:07:38

QT开发入门:QPushButton控件使用全指南

1. QT新手日记005:从零开始掌握QPushButton控件作为一名刚接触QT框架的开发者,我最近在项目中使用QPushButton控件时踩了不少坑。这篇日记记录了我从完全陌生到熟练使用这个基础控件的心路历程,特别适合那些和我一样刚开始接触QT界面开发的朋…

作者头像 李华
网站建设 2026/9/12 21:05:51

Cataclysm DDA 新手生存指南:3 个循环 + 5 条避坑原则

Cataclysm DDA 新手生存指南:3 个循环 5 条避坑原则 【免费下载链接】Cataclysm-DDA Cataclysm - Dark Days Ahead. A turn-based survival game set in a post-apocalyptic world. 项目地址: https://gitcode.com/GitHub_Trending/ca/Cataclysm-DDA Catacl…

作者头像 李华