:LangChain/LangGraph 与 Function Calling 的考点拆解)
1. 面试官为什么总在 LangChain 和 LangGraph 之间反复横跳如果你最近在面大模型 Agent 岗大概率会遇到这样的追问节奏先问你 LangChain 里 AgentExecutor 怎么跑等你答完 ReAct 循环面试官话锋一转——“那 LangGraph 呢什么时候该用它”再往后Function Calling 的 JSON Schema 怎么设计、NL2SQL 的 schema linking 怎么做、工具调用失败怎么回退一层层往下挖。这些问题的共同点是它们不考你背没背过 API而是考你有没有真正跑通过一条完整的 Agent 链路。LangChain 解决的是“让模型会用工具”LangGraph 解决的是“让多步骤流程可控可回溯”Function Calling 是两者共同的底层通信协议NL2SQL 则是检验这套链路是否真正落地的一道综合题。我试过用同一道 NL2SQL 题分别走工具调用和图编排两条路径输出差异非常明显前者一步到位但遇到歧义就崩后者能自我修正但链路更长。这篇文章就按“概念辨析—链路设计—踩坑复盘”三层展开给出可复制的自测清单和最小 Agent 链路配置示例帮你在面试里把这条线讲清楚也帮你在工程里真正跑起来。适合谁看准备 Agent 岗面试的工程师、正在搭 NL2SQL 或工具调用链路的开发者、以及想搞清楚 LangChain 和 LangGraph 边界的人。核心检索词就几个——LangChain、LangGraph、Function Calling、NL2SQL、Agent 链路设计。2. TaoToken 前置把模型接入这步先跑通不管你后面用 LangChain 还是 LangGraph第一步都是让模型能稳定响应。面试里经常有人卡在“我本地跑不通”上其实问题往往出在接入层。这里我用 TaoToken 做统一入口把 Base URL、API Key、Model ID 三件套配好后面所有链路都基于它。先说清楚它是什么TaoToken 是一个大模型 API 聚合入口你可以在一个控制台里拿到兼容 OpenAI 协议的 Key然后让 LangChain、LangGraph、Cline、Claude Code 这些工具都指向同一个 Base URL。对面试自测来说好处是你不用为每个框架单独配一套环境变量切换模型只改 Model ID 就行。适合谁需要快速验证 Agent 链路、又不想在接入层折腾太久的人。能做什么统一管理 Key、切换模型、查看调用日志配合 LangChain 的 ChatOpenAI 或 LangGraph 的模型节点直接使用。操作路径很直接打开官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册后进控制台在 API Keys 页面生成一个 Key。控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite API Keys 页面是 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。生成后你会得到类似sk-xxxx的字符串这就是后面配置里的TAOTOKEN_API_KEY。注意一点API 地址是 https://taotoken.net/api 这个不带 UTM 参数直接作为base_url使用。模型对话页面在 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 你可以先在网页里发一条消息确认 Key 有效再去写代码。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有各框架的配置示例。如果你后面要长期跑编码类 Agent可以看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。Claude Code 相关的接入在 https://taotoken.net/claude-code?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite 。这一步的目标只有一个拿到 Key确认模型能回话。别急着写 Agent先把接入层跑通后面排障会省很多时间。3. 可复制配置LangChain 工具调用 LangGraph 图编排最小链路这一节是全文的技术核心。我会给出两份可复制的配置一份是 LangChain 的 Function Calling 工具调用链路一份是 LangGraph 的状态图编排链路。两份都用同一道 NL2SQL 题来验证方便你对比输出差异。先看环境变量。我习惯用一个.env文件管理路径放在项目根目录# .env TAOTOKEN_API_KEYsk-你的key TAOTOKEN_BASE_URLhttps://taotoken.net/api TAOTOKEN_MODELgpt-4o-mini然后是 LangChain 侧的工具定义。这里用tool装饰器定义一个 NL2SQL 工具入参用 Pydantic 约束出参返回结构化结果# langchain_nl2sql.py import os from dotenv import load_dotenv from langchain_openai import ChatOpenAI from langchain_core.tools import tool from langchain.agents import AgentExecutor, create_openai_tools_agent from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder from pydantic import BaseModel, Field load_dotenv() class SQLQueryInput(BaseModel): question: str Field(description用户的自然语言问题) table_hint: str Field(default, description可选的表名提示) tool(nl2sql_query, args_schemaSQLQueryInput) def nl2sql_query(question: str, table_hint: str ) - str: 将自然语言问题转换为 SQL 并返回查询结果摘要。 # 这里替换成你真实的 schema linking SQL 生成逻辑 sql fSELECT COUNT(*) FROM orders WHERE status paid; return f生成的SQL: {sql}\n结果: 1284 条已支付订单 llm ChatOpenAI( modelos.getenv(TAOTOKEN_MODEL), api_keyos.getenv(TAOTOKEN_API_KEY), base_urlos.getenv(TAOTOKEN_BASE_URL), temperature0, ) prompt ChatPromptTemplate.from_messages([ (system, 你是一个 NL2SQL 助手先判断是否需要查询数据库再调用工具。), (human, {input}), MessagesPlaceholder(variable_nameagent_scratchpad), ]) agent create_openai_tools_agent(llm, [nl2sql_query], prompt) executor AgentExecutor(agentagent, tools[nl2sql_query], verboseTrue) if __name__ __main__: result executor.invoke({input: 上个月已支付的订单有多少}) print(result[output])这段代码的关键点args_schema用 Pydantic 约束入参create_openai_tools_agent走的是 OpenAI 兼容的 Function Calling 协议AgentExecutor负责 ReAct 循环。跑起来后你会看到模型先输出一个 tool_call再拿到工具结果最后组织成自然语言。再看 LangGraph 侧。同样的 NL2SQL 任务用状态图拆成“意图解析→schema链接→SQL生成→执行→复核”五个节点条件边控制是否需要重试# langgraph_nl2sql.py import os from typing import TypedDict, Literal from dotenv import load_dotenv from langchain_openai import ChatOpenAI from langgraph.graph import StateGraph, END from langgraph.checkpoint.memory import MemorySaver load_dotenv() class NL2SQLState(TypedDict): question: str intent: str schema: str sql: str result: str retry_count: int llm ChatOpenAI( modelos.getenv(TAOTOKEN_MODEL), api_keyos.getenv(TAOTOKEN_API_KEY), base_urlos.getenv(TAOTOKEN_BASE_URL), temperature0, ) def parse_intent(state: NL2SQLState) - NL2SQLState: resp llm.invoke(f判断这个问题是否需要查数据库只回答 yes 或 no{state[question]}) return {intent: resp.content.strip().lower()} def link_schema(state: NL2SQLState) - NL2SQLState: # 实际项目里这里查向量库做 schema linking return {schema: orders(id, status, created_at, amount)} def generate_sql(state: NL2SQLState) - NL2SQLState: prompt f根据表结构 {state[schema]} 生成 SQL问题{state[question]} resp llm.invoke(prompt) return {sql: resp.content.strip()} def execute_sql(state: NL2SQLState) - NL2SQLState: # 沙箱执行这里用模拟结果 if SELECT not in state[sql].upper(): return {result: SQL 语法校验失败, retry_count: state.get(retry_count, 0) 1} return {result: 1284 条已支付订单, retry_count: state.get(retry_count, 0)} def review(state: NL2SQLState) - NL2SQLState: return {result: f结论{state[result]}依据{state[sql]}} def should_retry(state: NL2SQLState) - Literal[generate_sql, review]: if 失败 in state.get(result, ) and state.get(retry_count, 0) 2: return generate_sql return review graph StateGraph(NL2SQLState) graph.add_node(parse_intent, parse_intent) graph.add_node(link_schema, link_schema) graph.add_node(generate_sql, generate_sql) graph.add_node(execute_sql, execute_sql) graph.add_node(review, review) graph.set_entry_point(parse_intent) graph.add_edge(parse_intent, link_schema) graph.add_edge(link_schema, generate_sql) graph.add_edge(generate_sql, execute_sql) graph.add_conditional_edges(execute_sql, should_retry, {generate_sql: generate_sql, review: review}) graph.add_edge(review, END) app graph.compile(checkpointerMemorySaver()) if __name__ __main__: config {configurable: {thread_id: nl2sql-demo-1}} out app.invoke({question: 上个月已支付的订单有多少, retry_count: 0}, config) print(out[result])两份配置的差异一眼可见LangChain 版本靠模型自主决定调不调工具链路短但不可控LangGraph 版本把每一步显式建模成节点条件边控制重试checkpointer 做线程级持久化。面试里被问到“什么时候用 LangGraph”你就拿这个对比讲需要多步骤、可回溯、有状态、要插人审的时候图编排更合适。如果你用 Cline 或 Claude Code 做本地验证配置里同样要写全三件套。以 Cline 的 MCP 配置为例settings.json里这样写{ mcpServers: { taotoken-nl2sql: { command: python, args: [langgraph_nl2sql.py], env: { TAOTOKEN_API_KEY: sk-你的key, TAOTOKEN_BASE_URL: https://taotoken.net/api, TAOTOKEN_MODEL: gpt-4o-mini } } } }Codex 的auth.json则是{ base_url: https://taotoken.net/api, api_key: sk-你的key, model: gpt-4o-mini }三件套缺一不可Base URL 指向 https://taotoken.net/api Key 用控制台生成的Model ID 按你实际选的填。CC Switch 里切换配置时也是这三个字段别只改 Key 忘了 Base URL。4. 验证请求同一道 NL2SQL 题跑两条路径对比输出差异配置写完后验证动作要具体。我用同一道题——“上个月已支付的订单有多少”——分别跑 LangChain 和 LangGraph 两条路径观察输出差异。先跑 LangChain 版本python langchain_nl2sql.py你会看到 verbose 输出里出现这样的结构 Entering new AgentExecutor chain... Invoking: nl2sql_query with {question: 上个月已支付的订单有多少} 生成的SQL: SELECT COUNT(*) FROM orders WHERE status paid; 结果: 1284 条已支付订单 上个月已支付的订单共有 1284 条。 Finished chain.关键观察点模型只调了一次工具拿到结果就直接组织答案。如果问题有歧义比如“上个月”没指明是哪个月它不会反问而是自己猜一个。这就是工具调用链路的典型特征——快但容错靠模型自身。再跑 LangGraph 版本python langgraph_nl2sql.py输出会经过五个节点最终打印结论1284 条已支付订单依据SELECT COUNT(*) FROM orders WHERE status paid;如果你把execute_sql里的模拟结果改成“SQL 语法校验失败”就能看到条件边触发重试generate_sql节点被再次调用retry_count递增。这就是图编排的价值失败可回退、状态可持久化、每一步可观测。验证成功的标志有三个一是模型返回了结构化 tool_call 而不是纯文本二是工具结果被正确回传并组织成自然语言三是 LangGraph 版本在 checkpointer 里留下了状态快照。你可以用app.get_state(config)查看当前线程的状态确认每一步的输入输出都被记录。如果要做更严格的对比可以准备一组测试题覆盖单表查询、多表联结、聚合、时间范围歧义四类分别跑两条路径统计成功率和平均步数。面试里被问到“怎么评估 Agent 链路”你就拿这个说成功率、步数、延迟三个指标配合人工抽检。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth这一节按真实报错来。你在配 LangChain 或 LangGraph 时大概率会遇到下面几类问题。第一类401 Unauthorized。报错长这样openai.AuthenticationError: Error code: 401 - {error: {message: Invalid API key}}原因通常是 Key 没读到或者.env没加载。排查顺序先确认load_dotenv()在ChatOpenAI初始化之前调用再确认环境变量名和代码里读的一致最后去控制台 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 确认 Key 没过期。注意 Base URL 要写 https://taotoken.net/api 少写/api或写成别的路径都会 401。第二类local proxy failed。这个报错通常出现在你本地有网络层拦截时APIConnectionError: Connection error. local proxy failed排查方向检查系统环境变量里有没有HTTP_PROXY、HTTPS_PROXY残留有就临时清掉确认base_url是 https 而不是 http如果用了公司网络确认出口策略允许访问。这类问题跟框架无关是接入层的事。第三类reading choices。这个报错出现在模型返回体解析阶段KeyError: choices 或 TypeError: NoneType object is not subscriptable原因一般是模型返回了非预期结构比如流式和非流式混用或者base_url指向了一个不兼容 OpenAI 协议的端点。排查确认base_url是 https://taotoken.net/api 确认model字段填的是控制台里支持的 Model ID别自己编。如果用了streamTrue确认下游解析逻辑支持 SSE。第四类OAuth 相关报错。如果你用 Claude Code 接入可能会看到OAuth token expired or invalid这时候别去折腾 OAuth 流程直接检查auth.json或环境变量里的三件套是否完整。Claude Code 的接入文档在 https://taotoken.net/claude-code?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite 里面有完整的配置示例。核心还是 Base URL Key Model IDOAuth 报错往往是 Key 没配对导致的连锁反应。第五类LangGraph 状态丢失。表现为app.invoke第二次调用时拿不到上一次的状态。排查确认compile时传了checkpointer确认config里的thread_id一致。没有 checkpointer 就没有持久化这是图编排和普通链式调用的关键区别。排障时建议按“接入层→框架层→业务层”的顺序查先确认模型能单独回话再确认工具能被调用最后确认业务逻辑正确。接入层的问题去接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 对照框架层的问题看 verbose 日志业务层的问题写单测。6. 面试自测清单与长期编码路径把上面的内容压缩成一份面试自测清单你可以对着逐条过概念辨析层LangChain 的 AgentExecutor 和 LangGraph 的 StateGraph 本质区别是什么Function Calling 和传统文本解析调用的边界在哪NL2SQL 里 schema linking 为什么比 SQL 生成更容易出错。链路设计层给你一道多表联结的 NL2SQL 题画出工具调用和图编排两条链路的节点图说明 checkpointer 在长对话记忆和人审插入里的作用设计一个工具调用失败后的重试策略说明重试次数和回退条件。踩坑复盘层401 和 local proxy failed 分别怎么排查reading choices 报错通常指向哪类配置问题LangGraph 状态丢失的常见原因。如果你能把这三层讲清楚并且现场跑通一份最小配置Agent 岗的技术面基本就稳了。验证模型响应可以直接用模型对话页面 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 快速确认长期跑编码类 Agent 的话Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 更适合持续使用。最后留一个实用技巧把 LangGraph 的app.get_state(config)输出打印出来对照每一步的输入输出你会发现很多“模型不听话”的问题其实是状态传递错了而不是模型能力不够。这个习惯能帮你在面试里讲出别人讲不出的细节。