Agent不会干活?我给它加了3个工具,从FC到MCP一条线看透工具链进化

0 阅读8分钟

Agent不会干活?我给它加了3个工具,从FC到MCP一条线看透工具链进化

昨晚凌晨我跑通了一个能调3种工具的Agent——查天气、查数据库、读文件。跑通那一刻才真正理解一件事:LLM自己啥也干不了,它只会下工单。 你的代码才是干活的人。

这不是什么新鲜道理,但手敲代码跑通闭环后,感受完全不一样。之前看文档觉得FC挺简单——定义函数、注册tool、两轮对话,概念三步就完了。实际写的时候踩了4个坑,还有一个是中文数据库的charset问题,排查半小时才定位到根因。

今天把完整过程写出来,从FC本地调函数到MCP协议通信,一条线看透Agent工具链是怎么从"一个人干活"进化到"团队协作"的。

第一个工具:天气API,跑通FC闭环

工具链的起点是Function Calling。原理我之前学过——LLM不调用函数,它只输出tool_calls意图,你的代码拿到意图执行后喂回去,LLM才生成最终回答。两轮对话,LLM只决定"调什么、传什么参数"。

这次我把它跑通了。用wttr.in的免费天气API(不用API Key,curl直接用),写了个get_weather函数,注册成FC tool,让LLM决定什么时候调:

tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "查询指定城市的当前天气信息,包括温度、天气描述和湿度",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "城市名称,如Beijing、Shanghai、New York"
                }
            },
            "required": ["city"]
        }
    }
}]

问"北京今天天气怎么样",LLM输出get_weather({'city': 'Beijing'}),代码执行拿到"30°C 晴 湿度24%",喂回去,LLM生成"北京今天晴,30度,湿度较低比较干燥"。

问"帮我写个快排"——LLM直接回答,不调工具。判断正确。

这里有个关键设计点:description字段是LLM决策的唯一依据。写清楚比写花哨重要。如果你只写"查天气"三个字,LLM可能把"帮我查一下明天穿什么"也理解成要调天气工具。写成"查询指定城市的当前天气信息",它就知道什么时候该调什么时候不该调。

第二个工具:MySQL查询,踩了中文charset的坑

天气API跑通后,第二个工具是让Agent查MySQL数据库。思路一样——定义query_database函数,注册成tool,LLM生成SQL,代码执行返回结果。

但这里我踩了一个特别隐蔽的坑。

Agent问"工程部有几个员工?平均薪资多少?",LLM生成的SQL完全正确:SELECT COUNT(*) FROM employees WHERE department = '工程部'。但返回结果是0条——"工程部"匹配不到。

排查过程:直接在Docker里用mysql命令行查,3条数据都在。Python连数据库查,0条。说明不是数据的问题,是连接层的问题。

最后定位到根因:Docker exec插入中文数据时,MySQL client charset默认是latin1,导致中文被双重UTF-8编码。数据库"看起来"有数据,HEX值却是损坏的——正确的"工程部"应该是E5B7A5E7A88BE983A8,实际存的是C3A5C2B7C2A5C3A7C2A8E280B9C3A9C692C2A8

修复两处:

  1. 删表重建,插入前执行SET NAMES utf8mb4;
  2. Python连接加charset="utf8mb4"参数
conn = mysql.connector.connect(
    host="localhost", port=3306,
    user="root", password="llm_learn_2026",
    database="llm_learn",
    charset="utf8mb4"  # 不加这句,中文WHERE匹配不到
)

这个坑在Agent查数据库时特别隐蔽——SELECT *能查到所有数据,但WHERE条件匹配不到中文值。如果你在做Agent+数据库的项目,记住这条:中文数据库必须显式指定charset=utf8mb4,从连接到插入到查询全程统一

另外还有个安全设计值得一提——query_database做了白名单校验,只允许SELECT语句:

if not re.match(r'^\s*SELECT\s', sql, re.IGNORECASE):
    return "错误:只允许SELECT查询,禁止INSERT/UPDATE/DELETE/DROP"
sql = sql.split(';')[0].strip()  # 截断分号防注入

LLM生成的SQL你没法完全信任。万一它生成个DROP TABLE,没有这层校验就完了。这是Agent安全的第一道防线——工具函数自己做输入校验,不指望LLM自觉

第三个工具:MCP Server,自描述才是真进化

天气和数据库两个工具都是FC模式——你自己写tool定义、自己写tool函数、自己写tool_map映射。三个"自己写"意味着三处硬编码。换个工具就要改代码。

MCP协议的核心优势是Server自描述。你不需要硬编码tool列表——Client连上Server后自动发现有哪些工具、参数是什么、返回什么格式。

我用Python的FastMCP库写了一个文件系统Server,两个工具:list_files(列出目录文件)和read_file(读取文件内容)。Server代码很简洁:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("filesystem-server")

@mcp.tool()
def list_files(directory: str) -> str:
    """列出指定目录下的所有文件和子目录名称"""
    # ...实现省略

@mcp.tool()
def read_file(filepath: str) -> str:
    """读取指定文件的文本内容,限制返回前500行"""
    # ...实现省略

mcp.run(transport="stdio")

@mcp.tool()注解注册工具,函数名、参数名、docstring自动打包成MCP的tool schema。transport="stdio"意味着Agent通过标准输入输出管道跟Server通信,不需要开HTTP端口。

Client连接Server后,日志直接告诉我们发现了什么:

[MCP发现] Server暴露了 2 个工具:
  - list_files: 列出指定目录下的所有文件和子目录名称
  - read_file: 读取指定文件的文本内容,限制返回前500行

这就是"自描述"——Client不需要提前知道Server有什么工具,连上就知道了。

调用方式也不同。FC是本地函数调用(tool_map[func_name](**func_args)),MCP是协议通信——Client把请求通过stdio管道发给Server,Server执行后返回结果。Server是独立进程,可以部署在不同机器上,可以热加载新工具,Client不需要改代码。

跑的时候问了两个问题:"列出agent目录下有哪些文件"和"读取weather_agent.py的内容"。LLM自动选择调list_files还是read_file,参数传得也对。整个流程跟FC一样是两轮对话闭环,但工具发现和调用方式完全不同。

FC到MCP:一条进化线

把三种工具放在一起看,进化路径很清晰:

维度FC本地调用MCP协议通信
工具发现硬编码tool定义Server自描述
调用方式本地函数调用stdio/SSE协议
扩展性加工具要改代码Server热加载
部署同进程独立进程/独立机器
安全工具函数自校验Server做校验

FC是"一个人干活"——你定义工具、写实现、做映射,全在同一个进程里。MCP是"团队协作"——Server专注提供能力(查文件、查数据库、调API),Client专注决策(LLM决定什么时候调什么),两者通过标准协议解耦。

Java直觉:FC像Spring的@RequestMapping(声明路由+同进程调用),MCP像微服务的RPC(标准协议+独立部署+服务发现)。

再往上看还有两层——Skills(可复用+可分享+可热加载的能力包,像Spring Starter)和A2A(Agent之间的协作协议,像服务间的消息队列)。四层进化线:FC→MCP→Skills→A2A,从被动工具到主动协作。

四个踩坑总结

根因怎么解决的
中文WHERE匹配不到Docker exec charset=latin1导致双重编码SET NAMES utf8mb4 + Python连接加charset
LLM第二轮用文本模拟调函数第二轮create没传tools参数加tools=tools
Tool description太模糊LLM乱调工具description写清楚触发条件和参数含义
Docker exec插中文数据损坏client charset默认latin1插入前SET NAMES utf8mb4

第二个坑最蠢——LLM在第二轮调用时没被传入tools参数,所以它"自作主张"用文本格式模拟调函数,输出了一个不规范的<function_call>标签。修复就是第二轮create也传tools=tools

Agent工具链的核心原则我总结三条:描述要精确(LLM靠description决策)、安全要自校验(工具函数自己防注入防越权)、协议要解耦(MCP的Server自描述比FC的硬编码更可扩展)。

下次会写Agent安全与容错——权限控制、人类确认、沙箱、输入校验、重试机制。这是工具链的另一半:工具能干活了,但怎么确保它不干坏事?有问题评论区聊。


本文基于LangChain4j Week5 Day5实战,代码跑在GLM-5模型上。所有踩坑均为真实经历。