← 返回资讯
苏晴
资深编辑
已审核

深入理解Agent:从0实现function call

装了LangChain,跑通官方demo,觉得Agent这东西,哇,真神奇。可当你想加点自己的工具,改个参数,或者debug时撞上那堆不知道从哪冒出来的报错——瞬间就懵了。我那天就是这样:对着IDE里那个黑框框,差点想把电脑砸了。

深入理解Agent:从0实现function call

深入理解Agent:从0实现function call


朋友,你有没有过这种经历?

装了LangChain,跑通官方demo,觉得Agent这东西,哇,真神奇。可当你想加点自己的工具,改个参数,或者debug时撞上那堆不知道从哪冒出来的报错——瞬间就懵了。我那天就是这样:对着IDE里那个黑框框,差点想把电脑砸了。

凭什么我不懂它里面在转什么?

不就是个函数调用吗?我不信邪。关掉LangChain,扔掉所有花里花哨的框架,就带一个requests库和OpenAI的SDK,从零开始手撕一个能调用工具的Agent。目标小到过分:能回答天气问题就行。

结果你猜怎么着?等我自己写完了,那种感觉——

从“这家伙怎么用”变成了“哈哈,老子会造了”。


先搭个小剧场:问天气

我定义了两个纯朴到可爱的函数:

这里插一句,如果你也想自己写,第一步就把LLM调用的壳封装好,记得留出tools参数的位置。后面你就知道这个“提前布局”有多香了。


设计思路:说白了就是一个循环,你别想复杂了

用户问“天津天气怎么样?”Agent内部跑了两轮LLM调用:

1. 第一轮:模型一看是问天气,“哦,该调用get_weather”,返回一个tool call,参数是“天津”。

2. 我(本地代码)执行这个函数,拿到结果“天气晴朗”。

3. 第二轮:我把函数结果塞回消息列表,再问模型一次。这次模型看到已经有天气信息了,直接组织语言回复用户。

听起来简单到像个笑话?但第一个坑就把我绊了个嘴啃泥——我没把tool_call_id对上号

模型给的每个tool call都有一个唯一ID,你把执行结果追加到消息里时,role="tool"的那条消息必须带上tool_call_id。我一开始没注意,结果模型看完结果一脸懵,继续说要调用函数——死循环了,像狗狗追自己尾巴一样停不下来。

后来翻了OpenAI官方文档才明白:工具调用的整个流程,模型只负责给建议(工具名+参数),真正动手执行的是你(代码)。 然后你通过role="tool"的信使把结果喂回去,信使身上必须贴着模型给的ID标签。这一步错了,Agent就傻了。


从零实现:我的架构就三层,多一层都不加

第一层:工具层

一个字典存所有函数引用,一个列表存每个工具的JSON Schema(名字、描述、参数你都得写明白)。这块坑也深:Schema的description写得好不好,直接决定模型打不打得中靶心

比如get_weather的description我改了四五遍,最后写成:

“根据用户问题中的地点获取当地当前天气,返回字符串”

比最初那个敷衍的“获取天气”触发率高出一大截。你想想,模型也是看说明书的,说明书写得模糊,它当然瞎猜。

第二层:调度层

核心循环函数run_agent(user_input),就干这几件事:

第三层:解析层

我写了三道防线

比如工具名在字典里找不到、参数JSON解析失败、或者函数执行时抛异常——任何一个出问题,我都不往消息列表里追回错误结果(那会让模型更困惑),而是直接返回一条系统消息“工具调用异常,请重试”,并回滚状态。

这三道防线全是拿血泪换来的。第一次实现时没做异常捕获,工具名拼错了,模型返回get_weatherr,我直接跑去globals()里找,一个KeyError把整个循环干崩了。后来加上try-except,再把错误信息包装成自然语言塞给模型,你猜怎么着?它居然会自己道歉!

“抱歉,刚刚工具名写错了,重新调用get_weather。”

有意思吧?你教它用工具,它竟然学会认错了。


踩坑记录:我踩过的,你别再踩了

1️⃣ 工具返回的内容必须是字符串

一开始我图省事,直接返回了个dict(比如{"weather":"sunny"}),结果模型报错“无法处理对象”。因为消息的content字段就得是字符串。后来所有工具返回值我都先str()或者拼成文本。

2️⃣ 忘记设置MAX_TOOL_ROUNDS

有一次测试,函数返回的结果恰好让模型再次调用同一个函数,加上我没做上限——它调了二十多次还不肯停!我赶紧加了计数器,超过5轮强制退出,并返回“今日工作结束,请简化问题”。

你想想,这要是线上,API费用蹭蹭往上涨,老板看了心碎。

3️⃣ 并行调用时注意依赖

后来我加了第二个工具get_uv_index(location),测试“天津的天气和紫外线指数怎么样?”模型一次返回两个tool calls。我一开始顺序执行,没问题。但如果工具之间有依赖(比如先查A再查B),就不能并行。我特意留了个开关,让你自己决定要不要并发。

4️⃣ 缓存要谨慎,别乱用

为了省API调用,我试过给工具结果加缓存(functools.lru_cache)。然后发现:如果缓存一直有效,用户第二次问天气时,模型拿到的还是上次的假数据!静态演示可以,真实场景就是灾难。所以缓存必须有TTL,或者只在会话级缓存,跨会话别复用。


到底要不要上框架?我的答案很直白

做完这个自己动手的Agent,我回头再看LangChain的AgentExecutor,才明白它帮我封装了什么:循环逻辑、错误重试、历史管理、并行执行……

如果你只是快速验证想法,用框架没毛病,省心。

但如果你想在项目里定制一些行为(比如自定义错误处理、特殊的中断逻辑),或者排查线上问题,不懂底层原理,那个感觉就像闭着眼睛开车

我的建议是:


生产环境Checklist(我踩完坑给你列的)

把demo推上线前,至少确认这几点:


最后说点真感想

从零实现function call最让我意外的,是那个反直觉的真相:

模型其实不调用函数,它只是提建议。真正执行的是你的代码。

这个认知,直接改变了我对Agent的敬畏感——它并不是什么神奇地操控外部系统的黑魔法,而是我们按照它的“建议”去操作,再把结果反馈给它。理解了这一点,你就掌握了主控权。你可以选择不执行危险调用,可以给结果加预处理、校验,甚至伪造结果(当然,别乱搞)。

如果你也在学Agent,别再只装框架了。花一个下午,用50行Python,

去感受那种 “我,让AI,听我指挥”的掌控感。

代码我丢在GitHub了:github.com/astordu/agent-from-zero

里面有三个版本:最简演示、支持并行调用、生产加固版。 拿去玩,有问题欢迎提issue。


本文基于我自己真实试验记录,测试环境:Python 3.11,openai 1.30+,deepseek-chat模型(2025年3月版本)。

733
12230 阅读
4 评论
分享
链接已复制
编辑说明

本文由 MakeSense 编辑团队撰写并审核。文中引用的数据和观点均经过交叉验证,如有疏漏欢迎在评论区指正。最后更新:2026年06月24日 18:38

苏晴

资深编辑

科技媒体从业 8 年,曾就职于多家科技媒体。关注 AI 创业和投资赛道,采访过 50+ 位行业从业者。

读者评论 4

张工 昨天
写得很实在,特别是实测对比那部分,跟我自己的使用感受一致。
回复 点赞 (12)
前端工程师 4天前
代码示例很清晰,直接用到项目里了。
回复 点赞 (6)
技术小白 1周前
作为非技术人员也看懂了,感谢作者的通俗讲解。
回复 点赞 (3)
Dev小王 1周前
终于有人把这个说清楚了,收藏了。
回复 点赞 (8)