自从上一篇文章《为什么你不应该选择LangChain》发布之后,我在公众号上收到的最多留言就是“应该选择什么?”以及“XXX框架怎么样?”,于是才意识到自己挖下了一个大坑。选什么以及怎么选无法一概而论,它依据你有待解决的问题而定,今天让我们从最简单的场景开始。
本文的目的不是教你用某个具体的AI框架开发某种具体类型的应用,而是会向你呈现一种通用的模式,在这个模式里选择什么样的AI框架并不重要。你可以把标题里的Supabase换成任意的PaaS(Platform as a Service)甚至是BaaS(Backend as a Service)服务,例如AppWrite、Vercel、Railway、Fly.io;你也可以把标题中的Gemini SDK换成OpenAI SDK、Claude SDK等等。
两种AI开发模式
让我从一个简单的真实案例开始说起。我自己有给自己制作了一个AI新闻采集工具,因为我只想看到来自主流媒体的一手专业报道,而不是各种推特上的小道消息,更不是平台各种强加于你的暴力推荐。

它的工作原理非常简单,定期拉去新闻网站的RSS数据进行更新,如果网站不提供RSS数据那么便使用浏览器模拟用户访问抓取。除此之外我还会使用perplexity提供的sonar-pro模型来对AI行业内发生的重大新闻进行解读
你会怎么实现这个工具?在我看来至少存在两种常见的实现思路:Agent模式与Workflow模式
Agent模式
Agent模式以AI为中心:我创建一个agent,在创建这个agent时用prompt去定义它的工作流,并且同时为其添加在工作流中可能会使用到的工具(函数)。借助OpenAI Agent SDK大致的实现代码如下:
const agent = new Agent({
name: 'Audio Transcript Assistant',
instructions: `
你是一个专业的AI新闻抓取助手,
请根据以下流程抓取指定新闻网站的新闻:The Verge, TechCrunch, The Vox...
- 优先使用网站提供的RSS数据源获取新闻资讯
- 如果网站尚未提供RSS数据,则使用浏览器模拟用户行为来抓取最新资讯
- 最后请使用perplexity提供的sonar-pro模型来对过去四小时内发生重大AI新闻进行解读
`,
model: "gpt-4.1",
tools: [
getNewsByRSS,
getNewsByChromeBrowser,
getNewsInsightsByPerplexity
],
});
在这里模式下我们像是甩手掌柜完全把“程序”交由Agent执行,我们仅仅是告诉它一个期待的执行流程以及过程中可能会用到的工具。至于它是如何将自然语言翻译成执行过程,又会决定在何时调用何种工具函数,至少在模型开始“思考”前我们无从知晓(当然执行过程中我们可以通过调试工具观测到)。
注意以上是一个agent略缩版,在真实场景中我们还要补充一些边界情况的处理逻辑,例如如何重试、如何响应失败;以及添加更多的工具函数帮助它对一些场景做精准判断。
工作流(workflow)模式
在上面Agent模式中,AI是不仅是程序的起点,还是程序的容器。而另一种可能性是把AI仅仅当作是程序的其中一个环节。如果你仔细观察我的产品需求,就不难发现真正需要使用到AI的环节,不过是请求perplexity的那一瞬间。于是程序完全可以使用传统程序来表达,大致代码如下:
const rssFeed = [];
const chromeSimulationFeed = [];
rssFeed.forEach(async rssUrl => {
await rssFeedCralwer.crawl(Url)
})
chromeSimulationFeed.forEach(async webUrl => {
await websiteCralwer.crawl(webUrl)
})
const completion = await client.chat.completions.create({
messages: "",
model: "sonar-pro"
})
const latestNewsInsights = completion.choices[0]?.message?.content as string ?? '';
工作流还存在另一种变种,就是借助AI框架来实现,例如LangGraph或者Mastra。以Mastra为例,我们可以将明显的独立代码逻辑封装在一个“步骤”中,再将多个步骤组合成一个“工作流”,大致代码如下 :
const crawlRssFeedsStep = createStep({
id: 'crawl-rss-feeds',
execute: async ({ inputData }) => {
return await Promise.all(
inputData.rssFeed.map(url => rssFeedCrawler.crawl(url)),
);
},
});
// const crawlChromeFeedsStep = ...
const generateInsightsStep = createStep({
id: 'generate-insights',
execute: async ({ inputData }) => {
const completion = await client.chat.completions.create({
messages: [{ role: 'user', content: allContent }],
model: 'sonar-pro',
});
return
(completion.choices[0]?.message?.content as string) ?? '';
},
});
export const newsCrawlerWorkflow = createWorkflow({
id: 'news-crawler-workflow',
})
.parallel([crawlRssFeedsStep, crawlChromeFeedsStep])
.then(generateInsightsStep)
.commit();
如何做技术选型
“简单”是我推崇的第一设计原则——别小看了这两个字,它能带来更低维护成本,更少的调试成本,以及更快的部署节奏——当我们拿着“简单”这块放大镜审视我们业务需求时,需要回答的第一个问题是:哪些是我们不需要的?或者反过来问:我们真的需要一个AI框架或者Agent框架吗?
在我看来新工具的引入或者替换是存在代价的,例如(团队的)学习成本、集成成本、回归测试的成本。本质上这是一种交换,我付出一定的代价,换来长远的收益。但是在上面的例子中,相比传统工作流,我看不到使用OpenAI Agent SDK进行开发的更高价值,除了体验了一把“高精尖”技术的瘾。使用agent模式进行开发会存在以下两个风险点。
第一个问题便是有待完善的技术生态
开发者需要解决的无非是两类问题:1)业务问题;2)工程问题。并且在在解决每一类问题是我们衡量的无非是1)能否达到效果;2)成本如何。如果两种方案在解决业务问题上差异不到,且编写成本接近,那么传统工作流应该是我们的第一选择。虽然“传统”这个词听上去有些“守旧”或者“过时”意味在里面,但别忘了它还依然表着成熟,代表着这一整套工作流程、或者理论框架是反复被人验证过的。技术在变但是我们有待解决的工程问题永远不会变:例如代码质量需要用单元测试来保障,又例如我们需要应用具有良好的可观测性,帮助我们排查问题以及查询日志。
所有这些问题不是单凭开发者一己之力就可以解决的,在当下的开发环境里云厂商、第三方服务以及开源工具在解决这些问题上起到的作用更大。也许你还不知道,如Railway这样的PaaS平台,早就可以做到一键从GitHub上导入你仓库里Python或者Node.js程序,自动识别入口文件与启动命令,且内置日志收集与查询工具。而反观Agent开发这侧,当你想把一个Agent应用部署到线上时,你更多依赖的是Agent框架本身的部署文档。不是说例如测试中的mock、日志收集在Agent开发模式中无法做到,而是做起来成本更高。
第二个需要考虑的点便是AI带来的不确定性
技术是有属于自己的舒适区的,AI善于解决的其实是开放性的、未知的问题。而新闻抓取其实是一个封闭性的问题,在解决这个问题的过程中的绝大部分步骤都是确定,例如抓取哪些网站的新闻,哪些新闻网站使用的是RSS源,哪些网站又必须使用浏览器来抓取——在确定的问题上,AI的创新发挥不出功效。
更严重的是,在封闭性的问题中引入不确定性的因素可能会带来负面效果:例如针对封闭性问题编写一段能够使工作流稳定输出的prompt成本,是一定比直接使用编程语言要高;又例如对于开放性问题,用户对于AI提供答案的多样性有很好的容忍度,但是对于封闭性问题,用户则不太希望看待他预期之外的回答。
这里我举一个agent 工作流的正面例子,我还有另一个agent帮助我review我即将发布的文章,我会向它提供需要被review文章的内容,并为其提供工具允许它以注释的方式给文章提出建议。(因为我的文章是使用Notion进行编写,所以可以很方便的通过Notion API获取到文章内容以及添加注释)。使用Mastra Agent编写的部分代码如下:
export const notionArticleReviewerAgent = new Agent({
name: "Notion Article Review Agent",
instructions: `
你是一个专业文章review助手,为我的文章给出改进建议,
工作流如下
- 根据用户提供的文章URL或者ID获取到文章内容。文章数据会以数组形式返回,
- 数组中每一个元素都是一个文章块(block),每一个“块”包含两部分内容
1)id:“块”的唯一标识;
2)text:“块”中的文字内容
- 总结整篇文章的中心思想与每个章节的表达主题
- 对每一个“块”中的内容进行评审,在评审的过程中需要参考的内容有:
- 确保块中内容与整篇文章主题相符
- 确保块中内容与块所属章节立意相符
`,
model: "google/gemini-2.5-pro",
tools: {
addCommentTool,
getNotionArticleTool
},
});
在这个场景中使用prompt明显会比使用传统代码成本低很多。上面的绝大部分步骤都不需要借助额外的工具函数来完成,AI知道天生就知道如何“总结”,知道什么是“章节”,知道如何遍历数组,知道应该对block对象中的text进行review,更重要的是,它解决的问题是开放性的且给出的中立的建议,你不会认为它是“错”的。
聊回LangChain与LangGraph
如果调用AI只是传统工作流中的一环,那么你需要的只是一个与AI模型的通讯中介,自然LangChain未必是你最好的选择。因为OpenAI、Gemini、Claude等主流AI厂商都推出自己官方SDK,第一方SDK已经确保我能够稳定访问它们的服务以及提供最新的特性。除非你在开发中有需要调用不同厂商的模型,有需要隐藏实现细节的需求,这时LangChain或者是Vercel的AI SDK才可能是你的选项。
在我看来将AI与传统代码结合的关键在于,将AI的自然语言(非结构化)输出转化为结构化输出。这让AI变成了彻底一个某种第三方依赖,和API请求和数据库查询无异。目前绝大部分的SDK都已经支持结构化输出(structured output)特性。即将结果以指定的JSON格式予以返回,这让AI与传统的代码的集成更加丝滑。以OpenAI SDK为例代码如下
import OpenAI from "openai";
import { zodTextFormat } from "openai/helpers/zod";
import { z } from "zod";
const openai = new OpenAI();
const CalendarEvent = z.object({
name: z.string(),
date: z.string(),
participants: z.array(z.string()),
});
const response = await openai.responses.parse({
model: "gpt-4o-2024-08-06",
input: [
{ role: "system", content: "Extract the event information." },
{
role: "user",
content: "Alice and Bob are going to a science fair on Friday.",
},
],
text: {
format: zodTextFormat(CalendarEvent, "event"),
},
});
const event = response.output_parsed;
那么借助框架来创建工作流是否值得?还是上面说那两个问题:它是否解决了我们的问题,它解决问题的成本多少
传统工作流代表的是一种“松耦合”类型的控制,它们能做的不过是顺序执行、条件判断、循环、也许还有并发。但是AI框架的内置工作流是为AI有关业务特殊设计的,例如它支持人工介入(human-in-the-loop),支持流程的暂停与继续,支持回溯(time travel),支持步骤之间的状态共享。
更重要的是它还支持实现Agent过程中的一些常见设计模式。在我新书《零基础自学AI应用开发书中》我借助LangGraph实现了一个网关/路由模式,在这个模式里AI 作为入口路由决策者,再决定用哪个工具或流程。部分代码如下:
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o")
class State(TypedDict):
question: str
messages: Annotated[list, add_messages]
related_docs: list
def tool_decider_router(state: State):
system_prompt = """
你是一个工具选择器,需要根据用户问题选择最合适的搜索工具。请严格按照以下规则进行选择:
1. 当用户问题明确涉及React开发、前端开发或相关技术栈时:
- 选择 vector_search_tool
- 该工具会从专门的前端开发文档向量库中检索信息
2. 当用户问题与React开发无关,但是与编程开发相关时:
- 选择 web_search_tool
- 该工具会从互联网搜索最新信息
3. 当用户问题完全和技术无关时:
- 选择 ask_AI_help_tool
- 该工具会直接使用AI进行回答
注意:
- 每个问题只能选择一个工具
- 选择必须基于问题的具体内容,而不是猜测
- 如果问题同时满足多个条件,优先选择更具体的工具(vector_search_tool > web_search_tool > ask_AI_help_tool)
"""
class Router(TypedDict):
next: Literal["vector_search_tool", "web_search_tool", "ask_AI_help_tool", "FINISH"]
result = llm.with_structured_output(Router) \
.invoke([
{"role":"system","content":system_prompt}] \
+ [{"role": "user", "content": state["question"]}
])
return result["next"]
如果你不需要这些与AI业务有关的技术特性,那么你也就不需要AI级别的工作流
结束语
LangChain的流行让我意识到一件事:也许一个框架的流行可能并不在于它真的遥遥领先,而是可能因为它出现的足够早,以及在(中文)市场上能够找到的参考资料和DEMO足够多。这一逻辑对许多其他商品也同样成立。无可否认这同样也是一种成功。
然而做技术选型不等于追求某种“技术时尚”——选择市面上最流行的就好了——而是关于“知己知彼”:即1)我需要解决什么样的问题;2)我愿意付出什么样的成本;3)以及市面上有哪些工具可用。可惜的是大部分人不会对这些事想的太多。

