用LangChain接入自定义工具:完成工具定义与调用结果核对
具体问题与完成目标
假设你正在为一个课程助手智能体接入“查询课程通知”的工具。你写好了函数,绑定了模型,但运行后发现两种典型问题:模型要么根本不调用工具,要么调用了工具却返回“用户未找到”这类不符合预期的结果。更麻烦的是,你无法判断问题出在工具定义、参数传递,还是模型对调用结果的解读上。
本文围绕一个最小闭环展开:定义一个“课程通知查询”工具,接入LangChain的Agent,然后系统化地核对工具是否被正确调用、参数是否正确、返回结果是否被模型正确使用。完成本文后,你能独立完成自定义工具从定义到验收的全流程,并掌握三种可复用的核对手段。
适用环境:Python 3.10+,LangChain 1.x(本文代码基于langchain-core1.6.5及以上版本),需要可用的Chat模型服务(OpenAI兼容端点)。如果你没有真实模型凭证,文中提供了模拟客户端方案,可以离线验证工具调用逻辑。
前置条件与案例输入
场景设定
你为一门“数据结构”课程开发智能体,需要查询以下虚构的课程通知数据:
| 通知主题 | 内容 | 发布时间 |
|---|---|---|
| 实验报告 | 第10周周五18:00前提交到课程平台 | 第8周 |
| 课程答疑 | 每周三19:00-20:00,线上会议室 | 第8周 |
| 期末复习 | 第16周发布资料,以平台通知为准 | 第8周 |
读者需要准备的输入:
- 一个可调用的Chat模型端点(模型标识和API Key从环境变量读取)
- Python环境与基础依赖
- 本文提供的全部代码文件
文件清单
| 文件 | 用途 |
|---|---|
course_tools.py | 定义自定义工具函数 |
agent_setup.py | 创建Agent并绑定工具 |
verify_calls.py | 执行验收测试 |
.env | 存放模型凭证(不提交版本控制) |
必要原理与方案选择
工具定义的两个核心要素
LangChain中定义自定义工具的最简方式是@tool装饰器。一个工具能被模型正确调用的前提是:函数签名提供参数类型,docstring提供语义说明。模型根据工具名称、描述和参数schema决定是否调用、传什么参数。
fromlangchain_core.toolsimporttool@tooldefquery_course_notice(topic:str)->str:"""查询课程通知。当用户询问课程相关的通知、截止时间或安排时使用此工具。 Args: topic: 通知主题,必须是以下之一:实验报告、课程答疑、期末复习 """...这里的docstring不是写给开发者看的注释,而是模型决定调用行为的依据。如果docstring只说“查询通知”而不说明何时该用、参数该传什么,模型的行为会变得不可预测。
为什么选择LangGraph的ToolNode
ToolNode是LangGraph预置的工具执行节点,它自动处理了工具调用的输入解析、异常捕获和ToolMessage构造。相比手动遍历tool_calls并逐个调用,ToolNode减少了遗漏tool_call_id关联、忘记捕获异常等常见错误。对于本文的核对目标,ToolNode还让我们能方便地拦截每一次工具执行。
完整实现
步骤一:定义工具
创建course_tools.py:
"""课程通知查询工具。数据为虚构演示数据。"""fromlangchain_core.toolsimporttool# 虚构课程通知数据,用于演示和验证COURSE_NOTICES={"实验报告":"实验报告需在第10周周五18:00前提交到课程平台。","课程答疑":"课程答疑安排在每周三19:00-20:00,地点为线上会议室。","期末复习":"期末复习资料将在第16周发布,以课程平台通知为准。",}@tooldefquery_course_notice(topic:str)->str:"""查询指定主题的课程通知。当用户询问课程安排、截止时间或通知内容时使用。 Args: topic: 通知主题,必须从以下值中选择:实验报告、课程答疑、期末复习。 """iftopicinCOURSE_NOTICES:returnCOURSE_NOTICES[topic]returnf"未查询到主题为'{topic}'的课程通知。可用主题:实验报告、课程答疑、期末复习。"关键设计说明:参数topic的取值约束写在docstring中,而不是用枚举类型强制。这样模型在传错参数时,工具会返回带提示的错误信息,而不是让框架抛出类型异常导致对话中断。这为后续的失败场景测试留下了观察窗口。
步骤二:创建Agent并绑定工具
创建agent_setup.py:
"""创建课程助手Agent,绑定自定义工具。"""importosfromlangchain.agentsimportcreate_agentfromlangchain_openaiimportChatOpenAIfromcourse_toolsimportquery_course_noticedefbuild_agent():"""构建并返回课程助手Agent。"""api_key=os.environ.get("OPENAI_API_KEY")base_url=os.environ.get("OPENAI_BASE_URL")# 可选,用于兼容第三方端点model_name=os.environ.get("MODEL_NAME","gpt-4o-mini")ifnotapi_key:raiseValueError("缺少OPENAI_API_KEY。请设置环境变量后重试。""如果你没有真实凭证,参见本文‘离线验证’部分使用模拟客户端。")model=ChatOpenAI(model=model_name,api_key=api_key,base_url=base_url,temperature=0,)agent=create_agent(model,tools=[query_course_notice],system_prompt=("你是一个课程助手。当用户询问课程通知时,使用query_course_notice工具查询。""如果工具返回'未查询到',告知用户该主题没有通知,并列出可用主题。"),)returnagent配置说明:
OPENAI_API_KEY:从模型服务商控制台获取。缺少此项时,代码会在构建Agent时抛出明确的错误,而不是在调用时产生难以理解的网络异常。OPENAI_BASE_URL:如果你使用兼容OpenAI接口的第三方服务,在此填写其端点。留空则使用官方默认端点。MODEL_NAME:默认gpt-4o-mini,可根据服务商支持调整。
步骤三:运行并观察
# run_demo.pyfromagent_setupimportbuild_agent agent=build_agent()result=agent.invoke({"messages":[{"role":"user","content":"实验报告什么时候提交?"}]})# 打印所有消息,观察工具调用过程formsginresult["messages"]:print(f"[{msg.type}]{msg.content[:200]}")ifhasattr(msg,"tool_calls")andmsg.tool_calls:print(f" -> 工具调用:{msg.tool_calls}")预期输出结构(取决于模型的实际行为,但应包含以下要素):
[human] 实验报告什么时候提交? [ai] -> 工具调用: [{'name': 'query_course_notice', 'args': {'topic': '实验报告'}, 'id': 'call_xxx'}] [tool] 实验报告需在第10周周五18:00前提交到课程平台。 [ai] 实验报告需要在第10周周五18:00前提交到课程平台。可操作的验收与测试
“能运行”不等于“正确”。以下三个场景帮助你判断工具接入是否真正可靠。
验收表
| 场景 | 测试目的 | 输入或操作 | 预期结果 | 判定方法 |
|---|---|---|---|---|
| 正常调用 | 模型在合适的请求下正确调用工具并传对参数 | “实验报告什么时候提交?” | 工具被调用,topic="实验报告",返回正确通知 | 检查ToolMessage内容与COURSE_NOTICES一致 |
| 边界调用 | 模型对模糊请求能合理判断或给出澄清 | “课程有什么安排?” | 要么调用工具查询最相关主题,要么回复要求用户指定主题 | 不接受“未查询到”被当作正常结果 |
| 失败调用 | 模型传入未定义参数时,工具优雅处理 | 手动构造topic="考试安排"的调用 | 返回“未查询到主题…可用主题:…” | 不抛出异常,消息中包含可用主题列表 |
验收脚本
创建verify_calls.py,直接测试工具本身和Agent的调用轨迹:
"""验收测试:覆盖正常、边界、失败三种场景。"""fromcourse_toolsimportquery_course_noticedeftest_normal_call():"""正常场景:工具能返回预定义的数据。"""result=query_course_notice.invoke({"topic":"实验报告"})expected="实验报告需在第10周周五18:00前提交到课程平台。"assertresult==expected,f"期望:{expected}, 实际:{result}"print("[PASS] 正常调用:返回内容与预定义一致")deftest_failure_call():"""失败场景:传入未定义主题时,不抛异常并给出提示。"""result=query_course_notice.invoke({"topic":"考试安排"})assert"未查询到"inresult,f"期望包含'未查询到', 实际:{result}"assert"可用主题"inresult,f"期望包含'可用主题', 实际:{result}"print("[PASS] 失败调用:优雅处理未知参数")deftest_agent_trajectory():"""Agent层验证:检查工具是否被调用、参数是否正确。 需要真实的模型凭证。如果没有,此测试标记为跳过。 """importosifnotos.environ.get("OPENAI_API_KEY"):print("[SKIP] 缺少OPENAI_API_KEY,跳过Agent轨迹测试")returnfromagent_setupimportbuild_agent agent=build_agent()result=agent.invoke({"messages":[{"role":"user","content":"课程答疑安排在什么时间?"}]})# 检查消息序列中是否包含工具调用tool_calls_found=[]tool_messages_found=[]formsginresult["messages"]:ifhasattr(msg,"tool_calls")andmsg.tool_calls:tool_calls_found.extend(msg.tool_calls)ifmsg.type=="tool":tool_messages_found.append(msg)assertlen(tool_calls_found)>=1,"期望至少有一次工具调用"asserttool_calls_found[0]["name"]=="query_course_notice",(f"期望工具名query_course_notice, 实际:{tool_calls_found[0]['name']}")asserttool_calls_found[0]["args"]["topic"]=="课程答疑",(f"期望topic='课程答疑', 实际:{tool_calls_found[0]['args']}")assertlen(tool_messages_found)>=1,"期望至少有一条ToolMessage"assert"19:00-20:00"intool_messages_found[0].content,("期望返回内容包含课程答疑时间")print("[PASS] Agent轨迹:工具调用名称、参数和返回内容均正确")if__name__=="__main__":test_normal_call()test_failure_call()test_agent_trajectory()执行命令:
cd<你的项目目录>pipinstalllangchain langchain-openai langgraph python-dotenv python verify_calls.py实际验证时,前两个测试不需要模型凭证,可以在任何环境下运行。第三个测试需要真实模型凭证。如果你只有模拟客户端,见下一节。
离线验证方案
当你没有真实模型凭证时,可以用模拟客户端验证工具调用逻辑的骨架。模拟客户端不替代真实模型验证,只用来确认代码路径没有断裂。
# mock_llm.py"""模拟Chat模型,用于无凭证时验证工具绑定和调用逻辑。"""fromlangchain_core.language_models.chat_modelsimportBaseChatModelfromlangchain_core.messagesimportAIMessagefromlangchain_core.outputsimportChatGeneration,ChatResultclassMockChatModel(BaseChatModel):"""测试桩:根据输入决定是否模拟工具调用。"""@propertydef_llm_type(self)->str:return"mock"def_generate(self,messages,stop=None,run_manager=None,**kwargs):# 检查是否有工具调用结果(ToolMessage)在历史中has_tool_result=any(getattr(m,"type",None)=="tool"forminmessages)last_user=Noneforminreversed(messages):ifgetattr(m,"type",None)=="human":last_user=m.contentbreakifnothas_tool_resultandlast_userand"课程"inlast_user:# 模拟模型决定调用工具ai_msg=AIMessage(content="",tool_calls=[{"name":"query_course_notice","args":{"topic":"实验报告"},"id":"mock_call_001",}],)elifhas_tool_result:# 模拟模型读取工具结果后生成回答ai_msg=AIMessage(content="根据通知,实验报告需在第10周周五18:00前提交。")else:ai_msg=AIMessage(content="你好,我是课程助手。")returnChatResult(generations=[ChatGeneration(message=ai_msg)])用模拟客户端替换ChatOpenAI,验证create_agent是否能正确接收工具调用、执行工具、把结果传回模型。这个测试的局限:它验证的是框架管道是否通畅,不能证明模型在真实场景下会选择正确的工具或参数。
常见故障定位
| 现象 | 可能原因 | 定位方法 |
|---|---|---|
| 模型从不调用工具 | 工具描述不够具体;system prompt没有引导 | 打印发送给模型的消息,检查工具schema是否包含;在system prompt中明确要求“询问通知时使用工具” |
| 工具被调用但参数错误 | docstring中参数说明模糊 | 在topic的docstring中列出允许值;在工具内部对未知值返回提示而非抛异常 |
| 模型忽略工具返回结果 | 工具返回内容过短或格式不规范 | 检查ToolMessage是否被正确附加到消息序列;确保返回内容是自包含的 |
ToolNode抛出异常 | 工具函数本身有未捕获的异常 | 先用verify_calls.py的test_normal_call和test_failure_call直接测试工具函数 |
验证状态
已完成的核验:
- 工具定义代码(
course_tools.py)的语法与导入路径检查通过。 query_course_notice.invoke()的正常与失败场景在本地Python环境执行通过,返回内容与预期一致。- Agent创建的代码路径在
langchain1.x的API签名下检查通过(create_agent接受model、tools、system_prompt参数)。 ToolNode的异常处理机制依据LangChain官方文档确认。
未执行的验证:
- 真实模型(OpenAI或兼容端点)的端到端Agent调用未在本文写作环境中执行,因为缺少可用的模型凭证。读者需要用自己的凭证运行
verify_calls.py的test_agent_trajectory。 - 模拟客户端的完整运行未执行(仅作为代码示例提供)。
- 不同模型(GPT-4o vs Claude vs 本地模型)的工具选择行为差异未测试。
参考资料:
- LangChain Tools 官方文档,核验日期2026-10-09,https://docs.langchain.com/oss/python/langchain/tools
- LangGraph ToolNode 文档,核验日期2026-10-09,https://mintlify.wiki/langchain-ai/langgraph/api/prebuilt/tool-node
- LangChain Building Custom Tools 文档,核验日期2026-10-09,https://mintlify.wiki/langchain-ai/langchain/advanced/custom-tools
- LangChain Agent Evals 文档(轨迹匹配),核验日期2026-10-09,https://docs.langchain.com/langsmith/trajectory-evals