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(1attempt) * 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 测试新策略中的使用权自研引擎用于生产环境的核心链路。