很多开发者抱怨 LangChain 抽象层多、调试链路长,一个简单工具调用场景也要绕好几层。本文梳理这类框架被吐槽的真实原因,给出以 Function Calling 为内核的最小 Agent 循环实现与代码骨架,并对比轻量框架与全功能框架的适用场景、模型支持差异和上线前必须检查的工程细节。
一个常见的技术选型纠结
做 AI 应用时,很多人第一步就是装 LangChain:文档多、示例全、什么都有。但真写起业务来,往往会遇到另一种体验——为了完成「模型调用一个函数、拿到结果再回答」这么简单的事,要穿过 Chain、Agent、Tool、Memory、Retriever、Runnable 一整套抽象,出问题时栈信息层层嵌套,很难定位是 prompt 写错了、工具描述写错了,还是框架内部的格式转换出了岔子。
于是有一类反向思路开始流行:不要全功能编排框架,只保留一个内核——模型的 Function Calling(工具调用)能力,外面套一个几十行的循环。有开发者按这个思路做了轻量 agent 框架,主打「一个文件看懂全部逻辑」,也引来了不少讨论:有人觉得优雅,有人质疑多模型支持不够,有人提醒最终效果其实取决于模型、prompt 和 workflow,而不是框架。下面把这些经验整理成一套可操作的判断和实现方案。
为什么全功能框架会显得难用
把常见吐槽归纳一下,大致是这几点:
- 抽象层与简单需求不匹配:业务只是「查个天气」「读个数据库」,却要按框架的心智模型去组织代码,理解成本高于收益。
- 版本迭代快:接口改名、包拆分、默认行为调整时有发生,网上教程和实际安装的版本对不上,入门者容易卡在 import 报错上。
- 错误信息不透明:模型返回格式不合法、工具参数解析失败这类问题,经常被包装成框架自定义异常,原始响应看不到。
- 调试链路长:一次请求经过多层封装,打印日志要开好几个开关,难以快速确认「模型到底收到了什么 prompt」。
需要说明的是,这些问题更多出现在「场景简单、团队规模小、需要快速验证」的阶段。当需求变成复杂的多步检索、多 agent 协作、大量第三方集成时,全功能框架的价值就会体现出来。
轻量方案的核心:一个工具调用循环
以 Function Calling 为核心的框架,本质只有三步:把工具描述成 JSON Schema 交给模型;模型决定调用哪个工具、传什么参数;执行工具并把结果回填给模型,直到它不再调用工具为止。
一个可运行的最小骨架大致如下(以 OpenAI 风格接口为例,其他厂商的 SDK 结构类似):
import json
from openai import OpenAI
client = OpenAI()
MODEL = "你账号下可用的支持工具调用的模型"
TOOLS = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市当前的天气",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string", "description": "城市名,如 上海"}},
"required": ["city"],
},
},
}]
def run_tool(name: str, args: dict) -> str:
if name == "get_weather":
return json.dumps({"city": args["city"], "temp_c": 27, "sky": "多云"}, ensure_ascii=False)
raise ValueError(f"unknown tool: {name}")
def agent_loop(messages: list, max_steps: int = 8) -> str:
for _ in range(max_steps):
resp = client.chat.completions.create(
model=MODEL, messages=messages, tools=TOOLS, tool_choice="auto"
)
msg = resp.choices[0].message
messages.append(msg.model_dump(exclude_none=True))
if not msg.tool_calls:
return msg.content
for call in msg.tool_calls:
args = json.loads(call.function.arguments or "{}")
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": run_tool(call.function.name, args),
})
return "达到最大步数仍未收敛"
关键点只有四个:tools 里声明工具、判断 tool_calls 是否为空、按 tool_call_id 回填结果、设置 max_steps 防止死循环。这段代码总共几十行,任何一步出问题都能直接打日志看到原始响应。
什么时候该自己写,什么时候该用框架
| 场景 | 建议 | 理由 |
|---|---|---|
| 单轮或少数几轮工具调用、工具不超过 10 个 | 自己写循环 | 代码量小,可控性最高 |
| 需要复杂 RAG、文档切分、向量库切换 | 用成熟框架或其生态组件 | 这些轮子自己造成本高 |
| 需要多 agent 编排、状态机、人工介入节点 | 全功能框架或工作流引擎 | 图结构编排不是几十行能覆盖的 |
| 团队已深度使用某框架,且线上稳定 | 维持现状 | 迁移收益低于风险 |
| 只想快速验证一个想法 | 轻量方案 | 半天能跑通,便于推翻重来 |
多模型支持:轻量方案最容易被质疑的点
全功能框架的一个真实优势是适配层多,切换供应商只改一个字符串。自己写循环时,不同厂商的工具调用协议格式并不完全一致,切换需要写适配代码。
更需要注意的是模型能力差异:工具调用在部分主流闭源模型上已经比较稳定,而一些开源模型、本地部署模型、小参数模型在「是否输出结构化调用」「参数是否符合 Schema」上波动明显,有的会用自然语言假装调用,有的会把参数写成非 JSON。判断一个模型是否适合你的场景,不要看宣传,直接拿自己的工具集跑几十条真实请求统计成功率。
如果产品需要多模型,实用做法是在自己的循环里加一层薄适配:把各家返回统一成内部结构(工具名、参数、调用 ID、文本内容),上层逻辑只依赖内部结构。这层代码通常一两百行,比引入整套框架轻,又保留了切换能力。
语言与生态:前后端怎么取舍
有人强调「应用开发要优先提供 JS/TS 版本」,这个观点在产品由前端主导时是成立的:类型系统、前端直接调用、npm 生态都更顺。但也不必因此重写整套逻辑。更常见的做法是:
- Python 侧承担模型调用与工具执行,用 FastAPI 之类暴露流式 HTTP 接口;
- 前端用 SSE 接收增量输出和「正在调用某工具」的事件,体验一样能做顺;
- 或者反过来,用 TS 写 agent 循环,复杂计算类工具转发给后端服务。
选哪种取决于团队谁在维护 prompt 和工具定义,而不是哪门语言更流行。
上线前必须补的工程细节
最小骨架只能跑 demo,真正上线还要处理这些:
- 参数校验:先用 JSON Schema 校验工具参数再执行,非法参数不要直接丢给数据库或外部接口。
- 超时与重试:模型请求、工具执行分别设超时;工具要有幂等设计,避免重试造成重复下单、重复写入。
- 步数与成本上限:除
max_steps外,记录 token 消耗,超阈值中断并给出兜底回复。 - 可观测性:把每轮的请求消息、模型原始返回、工具结果落盘或上报,出问题能回放。
- 工具描述质量:
description写得含糊是调用失败的头号原因,参数说明里给出示例值通常比堆砌术语有效。 - 失败兜底:模型坚持调用不存在的工具、或连续多轮不收敛时,要有确定的降级路径。
- prompt 与模型版本管理:prompt 和模型名称都当作配置管理,换版本前跑回归测试。
关于长时间在线运行
agent 类任务常常是长会话、长任务:等待模型返回、轮询外部接口、批量处理。如果跑在本地笔记本上,休眠、断网、切换 Wi-Fi 都会让请求中断,恢复后上下文和中间结果也未必完整。除了把状态持久化到数据库、用可断点续跑的设计之外,也可以把常驻进程放到网络稳定、长期不休眠的环境里跑,例如机房中的远程 Mac 或云主机,本地只做开发和查看结果,属于可选方案之一。
需要读者自行确认的地方
- 各模型当前支持的工具调用格式、并发限制、计费方式会变,动手前请以官方最新文档为准。
- 各家 SDK 的字段名和返回结构随版本调整,示例代码中的调用方式需要对照你实际安装的版本核对。
- 轻量框架是否适合你,取决于工具数量、编排复杂度和团队维护能力,这部分只能靠小范围试跑验证。
小结
LangChain 一类框架被吐槽难用,多数时候不是因为它写得差,而是它解决的问题比你的问题更大。如果需求集中在「模型 + 若干工具 + 短循环」,自己写一个以 Function Calling 为内核的循环,配上参数校验、超时重试和日志回放,往往比引入整套抽象更快也更好调试;如果需求涉及复杂检索、多 agent 编排和大量现成集成,成熟框架的生态依然更省时间。真正决定产品效果的是模型选择、prompt 质量和 workflow 设计——框架只是中间层,选一个团队能看懂、能定位问题的就够了。