欢迎光临
我们一直在努力

AI 项目中的技术选型复盘:为什么放弃了 LangChain 自研编排

AI 项目中的技术选型复盘:为什么放弃了 LangChain 自研编排

一、LangChain 的生产困境:调试一场 Prompt 链调用花了三个小时

LangChain 的定位是"LLM 应用开发框架",它提供了一套抽象——Chain、Agent、Tool、Memory——让开发者用声明式的方式组合大模型能力。Demo 阶段看起来非常高效,几行代码就能搭出一个带记忆的对话 Agent。但进入生产环境后,问题开始集中爆发。

第一次严重触发是在排查一个 RAG 链路的线上问题。用户反馈回答质量不稳定——有时候能检索到相关文档,有时候完全忽略检索结果。排查过程花了整整三个小时,不是因为逻辑复杂,而是因为 LangChain 的 Chain 调用链把 Prompt 拼接、检索调用、LLM 调用全部封装在框架内部,要理解一次请求的完整执行路径,需要在 LangChain 源码的多个模块之间跳转、打断点、看中间结果。框架的抽象层在这里不仅没有降低复杂度,反而在调试时变成了理解障碍。

更深层的问题在于三个层面。第一是版本兼容性。LangChain 在 0.1.x 到 0.3.x 之间经历了多次 API 重构,团队被迫在每个版本升级时同步修改业务代码——一个月内的版本迁移成本累积超过 3 人天。第二是 Prompt 管理不透明。LangChain 的 PromptTemplate 在内部做了很多隐式处理(变量注入、格式化、Chat Message 类型转换),开发者很难精确知道最终发给模型的是什么字符串。第三是流量控制能力缺失。框架没有提供原生的重试策略、超时控制和熔断机制,需要外部包装,而这种包装又和框架内部的状态管理产生冲突。

基础设施不需要漂亮话。一个框架能让开发者失去对执行流的控制权,本质上就是设计缺陷。

二、自研编排的核心原则:显式控制流 + 可观测性内建

放弃 LangChain 后,自研编排引擎的设计遵循三条核心原则:

显式控制流意味着编排逻辑以 DAG(有向无环图)的形式定义在 YAML 配置文件中。每个节点是一个独立步骤——Embedding 检索、Reranker 排序、LLM 生成——节点之间通过 depends_on 字段声明依赖关系,编排引擎按拓扑排序执行。每个节点的输入和输出都是确定的纯数据(JSON),不做隐式转换。

可观测性从引擎设计之初就内建。每个步骤执行完成后,自动记录以下指标:执行耗时、输入参数(截断后)、输出结果(截断后)、错误信息、Token 消耗量。这些数据通过 OpenTelemetry 导出到 Tracing 系统,一个请求的完整执行路径在 Jaeger 中是一条完整的 Trace,每个步骤是一个独立的 Span。排查问题时,不需要猜框架内部做了什么,直接看 Trace 就知道每一步的输入输出。

三、生产级实现:DAG 编排引擎与重试策略

编排引擎的核心是一个基于 DAG 的并发执行器。将编排定义解析为节点列表后,按拓扑顺序找出每一批可以并发执行的节点,并行执行,等待所有依赖完成后执行下一批:

// DAGExecutor DAG 编排执行器
type DAGExecutor struct {
nodes map[string]*PipelineNode
runner StepRunner
tracer trace.Tracer
}

// Execute 按拓扑排序执行所有未失败节点
func (e *DAGExecutor) Execute(ctx context.Context, input map[string]interface{}) (*PipelineResult, error) {
result := &PipelineResult{
StepOutputs: make(map[string]interface{}),
Metrics: make(map[string]*StepMetrics),
}

// 拓扑排序,确定执行批次
batches := e.topologicalSort()

for _, batch := range batches {
// 同批次节点并发执行
var wg sync.WaitGroup
errCh := make(chan error, len(batch))

for _, nodeID := range batch {
wg.Add(1)
go func(id string) {
defer wg.Done()

// 收集该节点的输入(来自前置节点的输出)
nodeInput := e.collectInput(result, e.nodes[id])

// 步骤级重试:最多重试 3 次,指数退避
var stepErr error
for attempt := 0; attempt < 3; attempt++ {
output, metrics, stepErr := e.executeStep(ctx, id, nodeInput)
if stepErr == nil {
result.StepOutputs[id] = output
result.Metrics[id] = metrics
return
}
time.Sleep(time.Duration(1<<attempt) * 100 * time.Millisecond)
}
errCh <- fmt.Errorf("步骤 %s 执行失败(重试3次): %w", id, stepErr)
}(nodeID)
}

wg.Wait()
close(errCh)

// 收集当前批次的错误
if err := <-errCh; err != nil {
result.Error = err
return result, err
}
}

return result, nil
}

出错处理是编排引擎和业务逻辑的分界线。编排引擎只负责执行流程控制和重试,业务逻辑中的语义错误(如检索结果为空)由编排配置中的 on_empty 策略处理——可以配置为跳过该步骤、使用默认值或终止整个管道。这个分离保证了引擎层代码的稳定性,业务逻辑的变更不影响引擎核心。

四、自研的代价:人力投入与生态缺失

自研编排引擎的决定不能理想化。投入成本需要被明确量化:开发核心引擎约 2-3 人月,调试和维护(包括新增步骤类型、性能优化、与下游服务适配)每月持续投入约 0.5 人月。对于一个 5 人的后端团队,这不是可以忽略的开销。

LangChain 生态中开箱即用的集成(各种向量数据库的 Connector、RAG 策略实现、Prompt 优化器)在自研方案中都变成了需要自己维护的代码。取舍原则是:只实现当前业务真实需要的集成,不为"将来可能需要"的 Connector 提前写代码。

自研编排引擎的禁用场景包括:团队规模小于 3 人、业务处于 POC 验证阶段、模型调用链路不超过 3 个步骤。在这些场景下,LangChain 或 LlamaIndex 的开发效率优势仍然大于运维成本。只有当日均调用量突破十万次、链路复杂度达到 5 步以上、排查一次线上问题的时间超过 1 小时时,自研的收益才会超过成本。

五、总结

放弃 LangChain 自研编排的决定依据是:当框架的抽象层成为排障的障碍而非辅助时,就该放弃了。自研方案的核心收益是显式控制流和可观测性的天然内建——排查问题不再需要猜测框架内部逻辑。

落地建议:先不要直接重写整个编排系统。从小范围试点开始——选择一条最复杂的推理链路(如 RAG 多路召回),用自研引擎重写,观察排查效率和开发效率的变化。如果试点效果显著提升,再逐步迁移其他链路。另外,保留 LangChain 在探索性场景(快速原型、A/B 测试新策略)中的使用权,自研引擎用于生产环境的核心链路。

赞(0)
未经允许不得转载:171主机测评 » AI 项目中的技术选型复盘:为什么放弃了 LangChain 自研编排
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址