
上一篇我以为 LangChain 就是调用大模型直到我写出第一条 Chain写完第一条 Chain 后我开始膨胀既然已经能让模型提取日志参数了那下一步不就是让模型自己调接口查日志吗我写了两个函数query_elk_mcp和query_zabbix_alert。然后直接把它们传给create_agent心想函数都写好了Agent 应该会自己调吧结果 Agent 完全无视它们。我盯着屏幕想了半小时才明白不是写了函数就能当 ToolLangChain 的 Tool 是有身份的。一、我当时想干什么我的目标是让 Agent 能够用户说查一下 web_scrm 最近 30 分钟的 ERROR 日志Agent 自己决定调用query_elk_mcp拿到结果后用 LLM 总结成一段人话返回我手上有两个现成的查询函数defquery_elk_mcp(log_level:str,service:str)-dict:模拟调用 ELK-MCP 查询日志。print(f正在查询{service}的{log_level}日志...)return{total:3,logs:[f{service}{log_level}日志 1,f{service}{log_level}日志 2,f{service}{log_level}日志 3,],}defquery_zabbix_alert(host:str,severity:str)-dict:查询指定主机的 Zabbix 告警。print(f正在查询{host}的{severity}告警...)return{total:3,alerts:[f{host}{severity}告警 1,f{host}{severity}告警 2,f{host}{severity}告警 3,],}然后我就天真地传给了 Agentagentcreate_agent(modelllm,tools[query_elk_mcp,query_zabbix_alert],# 没有 tool)结果 Agent 永远只会用自己的知识回答根本不会调用这两个函数。二、我写的代码正确的写法是给函数加上tool装饰器fromlangchain_core.toolsimporttooltooldefquery_elk_mcp(log_level:str,service:str)-dict:模拟调用 ELK-MCP 查询日志。 参数说明 - log_level: [str] 日志级别例如 ERROR、WARN、INFO - service: [str] 服务名例如 web_scrm 返回说明 - [dict] 包含 total 和 logs 列表 print(f正在查询{service}的{log_level}日志...)return{total:3,logs:[f{service}{log_level}日志 1,f{service}{log_level}日志 2,f{service}{log_level}日志 3,],}tooldefquery_zabbix_alert(host:str,severity:str)-dict:查询指定主机的 Zabbix 告警情况。 参数说明 - host: [str] 主机 IP 或主机名例如 127.0.0.1 - severity: [str] 告警级别例如 High、Average 返回说明 - [dict] 包含 total 和 alerts 列表 print(f正在查询{host}的{severity}告警...)return{total:3,alerts:[f{host}{severity}告警 1,f{host}{severity}告警 2,f{host}{severity}告警 3,],}agentcreate_agent(modelllm,tools[query_elk_mcp,query_zabbix_alert],)就这么一个toolAgent 突然就知道该什么时候调用了。三、我遇到的坑坑 1以为普通函数传给 Agent 就能用我最初没加tool直接把原生函数传进去agentcreate_agent(modelllm,tools[query_elk_mcp,query_zabbix_alert],)resultagent.invoke({messages:[{role:user,content:查 web_scrm 的 ERROR 日志}]})结果 Agent 的回复是我无法直接访问 ELK-MCP但我可以告诉你一般排查这类问题的思路...它根本没有尝试调用函数。因为 Agent 拿到的是两个普通 Python 函数看不到它们的参数说明、返回值类型也不知道什么场景该用哪个。tool的作用就是给函数发证告诉 Agent 这个函数叫什么、有什么用、需要什么参数、返回什么。坑 2docstring写得太敷衍Agent 乱调工具我一开始的query_elk_mcp只有一句话tooldefquery_elk_mcp(log_level:str,service:str)-dict:查询日志...然后我问web_scrm 服务最近有什么异常Agent 有时候调用query_elk_mcp有时候调用query_zabbix_alert完全随机。后来我才知道Agent 选 Tool 完全靠 docstring 里的描述。“查询日志” 和 “查询告警” 在它看来都符合异常这个词它就瞎猜。把 docstring 写具体后准确率才上去模拟调用 ELK-MCP 查询日志。 参数说明 - log_level: [str] 日志级别例如 ERROR、WARN、INFO - service: [str] 服务名例如 web_scrm 返回说明 - [dict] 包含 total 和 logs 列表 坑 3参数类型写错Agent 传参失败我有一次写了个工具参数类型标的是int但实际上应该传字符串tooldefquery_by_port(port:int)-dict:按端口查询服务return{result:fport{port}}结果 Agent 调用时传了一个带引号的字符串3306LangChain 直接报错ValidationError: Input should be a valid integer, unable to parse string as an integer类型提示不只是给 IDE 看的Agent 也会根据类型提示决定怎么传参。写工具时类型一定要和实际入参一致。坑 4直接调用函数和tool.invoke()搞混Tool 被装饰之后直接调用会出问题# 错误tool 装饰后的对象不是原来的函数resultquery_elk_mcp({log_level:ERROR,service:web_scrm})报错TypeError: query_elk_mcp() takes 2 positional arguments but 1 was given因为tool把函数包装成了一个StructuredTool对象。正确的调用方式是resultquery_elk_mcp.invoke({log_level:ERROR,service:web_scrm,})四、搞清楚后的结论tool不是语法糖它是 LangChain 里函数和 Agent 之间的翻译官。tool 装饰原始 Python 函数StructuredTool 对象Tool NameDescription来自 docstringArgs Schema来自类型提示Return TypeAgent 决定何时调用Agent 看到一个 Tool 时实际看到的是字段来源作用name函数名调用时的标识descriptiondocstringAgent 判断是否该用这个工具args_schema类型提示Agent 知道要传什么参数所以写 Tool 的核心就三件事函数名要清晰docstring 要写清楚使用场景类型提示要准确五、我的收获Agent 不会读你的代码它只读你给它的 Tool 描述。我以前总觉得函数逻辑都写对了Agent 应该能看懂吧事实不是。Agent 和函数之间隔着一个上下文窗口它只能看到你暴露出来的名字、描述、参数类型。如果你的 docstring 写得模糊Agent 就像拿到了一份没说明书的工具只能用猜的。写好 Tool是写好 Agent 的第一步。下一篇我会写 Agent 本身——为什么我用了create_agentAgent 还是不按我想的路线走。