02 · LangChain 基础
从手写循环转到 LangChain
Section titled “从手写循环转到 LangChain”第一课直接操作 Chat Completions 的消息字典和工具定义。LangChain 提供 Python 消息类型、工具包装器和模型适配器,让常见操作有统一接口。它不会替应用决定工具权限,也不会把一次模型请求变成一个自动可信的 Agent。理解底层消息协议仍然重要:当调用失败时,你需要知道模型收到哪些工具声明、返回了什么调用、宿主实际执行了什么,以及工具结果如何回填。
本仓库的 lessons/02_langchain/main.py 是一个离线契约练习,live.py 是可选实时路径。两者都使用 LangChain 的工具和消息类型,但用途不同:前者不调用模型服务,后者会向你配置的服务发送请求。先完成离线练习,再决定是否要测试实时路径。
工具函数如何变成可描述的工具
Section titled “工具函数如何变成可描述的工具”现有代码用 @tool 包装普通 Python 函数:
@tooldef add_numbers(left: int, right: int) -> int: """Add two whole numbers and return their sum.""" return left + right这个摘录来自仓库现有 main.py。LangChain 根据函数签名和说明生成工具输入 Schema,工具名为 add_numbers,参数 left、right 是整数。工具定义可以交给聊天模型,让模型生成符合格式的调用提议。工具实现仍是本机 Python 函数,最终执行发生在调用 add_numbers.invoke(...) 的进程中。
bind_demo_tool() 创建 ChatOpenAI,再调用 model.bind_tools([add_numbers])。绑定表示将工具声明附加到模型调用配置;绑定这一步既没有发送请求,也没有执行函数。本练习传入占位 Key local-demo-no-request,只是满足客户端构造器的配置要求。源码注释明确说明 bind_tools() 不发网络请求,也不需要真实 Key。不要把占位 Key 挪到实时示例中。
离线练习实际测试什么
Section titled “离线练习实际测试什么”从仓库根目录安装可选依赖并执行:
uv sync --extra exercisesuv run --extra exercises python lessons/02_langchain/main.pyrun_demo() 构造两条脚本化 AIMessage:一条请求 add_numbers(left=17, right=9),另一条传入字符串 "seventeen" 代替整数。代码没有调用 bound.invoke(...),也没有调用模型的 .invoke(...);它直接把手写的 AIMessage 交给 handle_tool_call()。因此输出中的成功和错误是本地真实执行 LangChain 工具/消息契约的结果,但不是模型根据提示生成的响应。
成功路径调用 add_numbers.invoke(call["args"]),得到整数 26,再封装为 ToolMessage:
ToolMessage( content='{"ok": true, "result": 26}', tool_call_id="call-ok", name="add_numbers",)这个消息片段依据现有代码和固定输入整理。ToolMessage 的 tool_call_id 回指 AIMessage 中 ID 为 call-ok 的工具调用;content 是发回模型的工具观察结果。失败路径会捕获工具参数验证异常,返回形如 {"ok": false, "error_type": "...", "error": "..."} 的内容,并将 ID 设为 call-bad。具体验证异常文字由安装版本产生,不应依赖一整段固定报错字符串。
脚本还打印绑定后的 Schema,成功工具响应和参数验证错误,最后明确打印:No provider request was sent; this is a local contract demonstration. 实际 Schema JSON 的字段顺序或附加描述可能受 LangChain 版本影响;应关注工具名、字段名和类型,不要把序列化顺序当协议语义。
这个离线练习覆盖了“构造工具定义”和“执行已给定工具调用”,没有覆盖模型规划、网络错误、供应商兼容性、完整对话循环或模型最终回答。它是练习工具输入输出的可复现环境,不是实时 Agent 的替代结果。
实时路径:bind_tools 之后仍有宿主循环
Section titled “实时路径:bind_tools 之后仍有宿主循环”在本机终端设置供应商凭证和模型名后,运行:
export OPENAI_API_KEY='在本机终端设置你的凭证'export OPENAI_MODEL='供应商支持的模型名称'# 使用 DeepSeek 时按 README 配置兼容 API 地址:export OPENAI_BASE_URL='https://api.deepseek.com'uv run --extra exercises python lessons/02_langchain/live.py 'Use add_numbers to calculate 17 + 9.'占位文字不是有效凭证,不要把真实 Key 粘到聊天或仓库里。live.py 从环境变量读取配置,用 ChatOpenAI(...).bind_tools([add_numbers]) 构造模型,再把 HumanMessage 作为消息列表调用 model.invoke(messages)。这次 .invoke() 才发起真实模型请求,并可能产生费用。返回的 AIMessage 会被追加到历史;没有 tool_calls 时程序返回模型文本;有调用时,宿主检查工具名、调用 add_numbers.invoke(...),再用 ToolMessage 将结果和 tool_call_id 放回消息列表,开始下一轮。
实时脚本只暴露 add_numbers,不提供文件、Shell 或网络访问工具。它最多运行 4 轮,并为模型请求设置 30 秒超时、关闭客户端自动重试。它逐个处理响应中的工具调用;未知工具返回受控错误,不调用任意函数。参数错误会回填 error_type,但这个实时分支只保留异常类型,不把异常详细文本发回模型。达到轮数限制时,脚本抛出错误而不是伪造最终答案。
离线和实时路径的核心分界是响应来源:离线 main.py 人工构造 AIMessage,用于测试工具执行及 ToolMessage;实时 live.py 通过模型适配器取得 AIMessage,由宿主执行调用并继续对话。两条路径都不能把 bind_tools() 本身理解成执行工具。
用小实验读懂调用合同
Section titled “用小实验读懂调用合同”- 运行离线示例。**完成标准:**输出含绑定工具 Schema、成功结果中的
26、非法参数错误及“没有发送供应商请求”的说明;你能指出代码里固定构造AIMessage的位置。 - 阅读
bind_demo_tool()和handle_tool_call()。**完成标准:**能解释bind_tools([add_numbers])声明什么,add_numbers.invoke(...)才做什么,以及ToolMessage.tool_call_id为何要保留原 ID。 - 改变
main.py中两条脚本化调用参数,例如改成left=12, right=9。**完成标准:**成功结果随固定输入变为 21;无效类型仍生成失败消息。不要声称这是模型根据新提示作出的决定。 - 加一个
multiply_numbers(left: int, right: int)工具,并在run_demo()中手写一条相应 AIMessage。**完成标准:**工具描述能出现在绑定 Schema 中,宿主只允许明确支持的名称,工具结果有匹配调用 ID。此练习修改仓库代码前先保留原文件或在自己的分支操作;本教程本身不要求你改动源文件。 - 若拥有凭证且愿意产生一次真实请求,运行
live.py的算式提示。**完成标准:**最终数值应为 26;若模型提出工具调用,终端还会显示round 1: ... tool request(s)一类日志。bind_tools不强制模型调用工具,所以只看到最终回答也可能是正常路径。没有凭证时跳过,不需要提交 Key 或向他人索取。 - 在实时提示中加入无关问题,比较模型直接回答和请求工具时的 trace。**完成标准:**能指出
tool_calls为空时循环为何返回,以及参数校验出错时如何转成工具观察消息。
No module named langchain_core或langchain_openai:未安装可选练习依赖。回到仓库根目录运行uv sync --extra exercises,再用同一uv run --extra exercises执行。- 离线脚本打印出工具 Schema,但看不到模型行为:这是设计结果。
main.py没发模型请求;Schema 来源于绑定对象,工具调用来源于源码构造的AIMessage。 Set OPENAI_API_KEY and OPENAI_MODEL in your shell:实时脚本当前进程未收到变量。检查在当前终端设置并在同一终端执行;不要打印 Key 来排查。- 401、连接或模型错误:检查凭证、模型名、供应商端点和网络。DeepSeek 路径依赖
OPENAI_BASE_URL=https://api.deepseek.com;本练习只在真实运行前对当前供应商兼容性做验证。 - 模型直接回答,没有工具日志:
bind_tools并不强制模型调用工具,模型可以直接回答。提示明确要求使用add_numbers,并确认供应商支持工具调用。 - 参数类型错误:工具输入 Schema 不代表所有响应均合格。检查模型给的
args和工具验证错误;保留受控失败路径,不要用宽松转换把任意字符串默默当作整数。 - 达到 4 轮限制:检查每轮消息和工具结果,任务可能持续请求工具。不要直接删掉限制;先确认工具结果能帮助模型结束,并根据预算定义新的停止规则。
LangChain 抽象的适用范围
Section titled “LangChain 抽象的适用范围”@tool 和 bind_tools 减少了手动拼接工具 Schema 的工作,消息对象让 AIMessage 与 ToolMessage 的语义更明确。排查底层问题时仍要看供应商协议:工具是否传入请求、模型返回的工具调用字段、调用 ID 与工具消息如何对应。还需单独实现实际执行策略、授权、超时、资源限制和副作用控制。
Schema 校验只回答输入是否符合结构约束。例如整数参数拒绝 "seventeen",不代表数值本身正确,也不证明调用符合用户授权。检索器、结构化输出或其他框架抽象同样不会自动保证来源准确或内容真实。遇到接口变化,核对仓库锁定的实际版本和当前官方文档,不要把旧教程 API 当成当前事实。
本课只验证 Python 路径。网站在浏览器中运行的 Tool Calling 实验使用 JavaScript 直接请求供应商;它不是 LangChain Python,也没有执行这些 Python 工具。
问:bind_tools()、工具调用提议和工具执行分别在哪里发生?
bind_tools() 在应用侧把工具定义附加到模型调用配置;模型收到定义后可以在响应中提出工具调用;宿主应用解析响应并调用本地工具实现。绑定不等于执行,模型也没有因声明而取得应用权限。
问:离线 LangChain 示例为什么不算模型 Agent?
它虽然用 LangChain 的真实工具和消息类型,但 AIMessage 是源码手写的,bind_tools() 和工具执行之间没有模型网络请求。示例验证工具 Schema、参数错误和 ToolMessage 合同,不验证模型的规划或最终回答。
问:工具调用结束后为什么仍要理解原始消息协议?
框架适配器最终仍需把工具描述、assistant 调用和工具结果转换成供应商能理解的请求。理解角色、调用 ID 和结果消息能定位格式或适配器故障,并帮助确认宿主是否执行了模型真正请求的工具。
问:参数通过 Schema 校验能说明什么,不能说明什么?
它说明数据符合指定的字段和类型约束。它不能证明数据为真、调用合乎授权、操作安全或工具结果正确。授权和副作用控制必须由宿主另行实现。