
Agent 项目技术选型复盘6 个工程决策的当时理由与事后验证一、深度引言与场景痛点最近翻了一下我们Agent项目从立项到现在的技术文档发现前前后后做了不下30个技术决策。有些决策事后看是英明神武有些则是当时觉得没问题、现在看来很天真。最有意思的是几个当时纠结了很久的决策——LangChain vs LlamaIndex、向量数据库选型、多Agent协作模式——有些到今天还有争议。我觉得复盘这些决策比单纯分享最后选了啥更有价值。因为每个项目的约束条件不同团队规模、时间压力、数据量级、预算限制技术选型没有银弹但决策逻辑是可以复用的。这篇文章复盘6个关键的工程决策每个决策都包含当时的选择理由、半年的运行验证、以及如果有机会重来会怎么做。二、底层机制与原理深度剖析技术选型不是什么技术方案PK赛而是一个多约束条件下的权衡过程。我画了一个决策模型决策不是一次性事件而是一个循环做决策→记理由→实施→监控→定期复盘。很多团队做完决策就忘了当初为什么这么做等半年后发现不对劲却想不起来当时的限制条件是什么。所以记录决策理由这步和做决策本身一样重要。三、生产级代码实现 技术决策记录工具 - Architecture Decision Record (ADR) import asyncio import json from datetime import datetime, timezone from dataclasses import dataclass, field from enum import Enum from pathlib import Path from typing import Optional class DecisionStatus(Enum): PROPOSED proposed ACCEPTED accepted SUPERSEDED superseded DEPRECATED deprecated dataclass class DecisionFactor: name: str weight: float description: str dataclass class Option: name: str description: str pros: list[str] cons: list[str] scores: dict[str, float] dataclass class ArchitectureDecisionRecord: ADR格式的技术决策记录 adr_id: str title: str status: DecisionStatus context: str decision: str options_considered: list[Option] factors: list[DecisionFactor] consequences: str created_at: datetime reviewed_at: list[datetime] field(default_factorylist) superseded_by: Optional[str] None lessons_learned: Optional[str] None class ADRManager: 技术决策管理器 def __init__(self, adr_dir: str ./docs/adr): self.adr_dir Path(adr_dir) self.adr_dir.mkdir(parentsTrue, exist_okTrue) async def record_decision(self, adr: ArchitectureDecisionRecord) - str: 记录一个技术决策 filename f{adr.adr_id}-{adr.title.lower().replace( , -)[:50]}.md content f# ADR-{adr.adr_id}: {adr.title} **状态**: {adr.status.value} **日期**: {adr.created_at.strftime(%Y-%m-%d)} ## 四、边界分析与架构权衡 {adr.context} ## 五、总结 for factor in adr.factors: content f- {factor.name} (权重: {factor.weight}): {factor.description}\n content \n## 考虑的方案\n\n for i, option in enumerate(adr.options_considered, 1): content f### 方案{i}: {option.name} {option.description} **优势**: for pro in option.pros: content f- {pro}\n content \n**劣势**:\n for con in option.cons: content f- {con}\n if option.scores: content \n**因素评分**:\n for factor, score in option.scores.items(): content f- {factor}: {score}/10\n content \n content f## 最终决策 {adr.decision} ## 影响与后果 {adr.consequences} if adr.lessons_learned: content f\n## 经验教训\n{adr.lessons_learned}\n if adr.superseded_by: content f\n**此决策已被 [{adr.superseded_by}] 替代。**\n filepath self.adr_dir / filename filepath.write_text(content, encodingutf-8) return str(filepath) async def review_decision( self, adr: ArchitectureDecisionRecord, lessons: str, new_status: Optional[DecisionStatus] None, ) - ArchitectureDecisionRecord: 复盘一个技术决策 adr.reviewed_at.append(datetime.now(timezone.utc)) adr.lessons_learned lessons if new_status: adr.status new_status await self.record_decision(adr) return adr async def supersede_decision( self, old_adr: ArchitectureDecisionRecord, new_adr: ArchitectureDecisionRecord, ) - tuple: 用新决策替代旧决策 old_adr.status DecisionStatus.SUPERSEDED old_adr.superseded_by new_adr.adr_id new_adr.context f\n\n此决策替代了 ADR-{old_adr.adr_id}。 await self.record_decision(old_adr) await self.record_decision(new_adr) return old_adr, new_adr def create_six_real_decisions() - list[ArchitectureDecisionRecord]: 创建6个真实的Agent项目技术决策 decisions [] # 决策1: LangChain vs LlamaIndex decisions.append( ArchitectureDecisionRecord( adr_id001, titleAgent框架选型, statusDecisionStatus.ACCEPTED, context我们需要一个框架来协调LLM调用与工具链。两个主要候选者 LangChain和LlamaIndex。项目启动于2024年初团队3人Python经验丰富。 产品需求客服Agent需要工具调用、多轮对话、动态知识注入。, decision选择LangChain因为其Agent/Tool抽象更成熟社区更大。 LlamaIndex当时更适合数据索引场景而非通用Agent场景。, options_considered[ Option( nameLangChain (v0.1), description通用LLM应用框架, pros[ Agent/Tool抽象成熟, 社区活跃2024初已有80k stars, 文档丰富教程多, ], cons[ 抽象层太厚调试困难, 版本迭代快API不稳定, LangSmith额外收费, ], scores{团队熟悉度: 8, 社区生态: 9, 灵活性: 7}, ), Option( nameLlamaIndex, description数据索引和数据连接的LLM框架, pros[ 数据摄取和检索的抽象更好, 文档结构清晰, ], cons[ Agent功能当时不够成熟, 工具调用支持较弱, 社区规模较小, ], scores{团队熟悉度: 5, 社区生态: 5, 灵活性: 6}, ), ], factors[ DecisionFactor(团队熟悉度, 0.3, 团队对框架的学习成本), DecisionFactor(社区生态, 0.3, 遇到问题时的解决方案可搜索性), DecisionFactor(灵活性, 0.4, 框架对自定义需求的适应能力), ], consequencesLangChain确实解决了Agent的核心需求 但过度抽象导致调试困难。后期团队花了大量时间理解LangChain的源码。 如果重来会在LangGraph的基础上做更薄的封装。, created_atdatetime(2024, 1, 10, tzinfotimezone.utc), lessons_learned框架选择不仅要看功能匹配度还要评估调试成本—— 抽象层越厚出问题时定位越难。, ) ) # 决策2: 向量数据库选型 decisions.append( ArchitectureDecisionRecord( adr_id002, title向量数据库选型, statusDecisionStatus.ACCEPTED, context我们需要存储和检索文档的向量表示。初期数据量约10万条chunk 预期6个月内增长到100万条。QPS约200。预算有限希望尽量复用已有基础设施。, decision初期选择Redis RediSearch复用已有Redis 数据量超过50万或QPS超过500时迁移到Qdrant。, options_considered[ Option( nameRedis RediSearch, description复用现有Redis集群加上向量搜索模块, pros[ 无需额外部署运维成本低, 团队对Redis运维经验丰富, 前期数据量下性能足够, ], cons[ 内存成本高向量数据全内存, OOM风险大向量和缓存混用, 功能不如专用向量数据库丰富, ], scores{运维成本: 9, 性能: 7, 扩展性: 5}, ), Option( nameQdrant, description专用向量数据库, pros[ 针对向量搜索深度优化, 支持磁盘存储内存效率高, 过滤搜索体验好, ], cons[ 需要额外部署和维护, 团队需要学习新工具, 与现有监控体系集成需要额外工作, ], scores{运维成本: 5, 性能: 9, 扩展性: 9}, ), ], factors[ DecisionFactor(运维成本, 0.4, 初期团队小需要减少运维负担), DecisionFactor(性能, 0.3, 满足当前和近期QPS需求), DecisionFactor(扩展性, 0.3, 支持数据量增长), ], consequencesRedis方案在前3个月运行良好但第4个月 因为批量导入文档缓存压力导致了一次OOM事故。 目前已规划迁移到Qdrant预计下个迭代完成。 如果重来会在项目开始就直接上Qdrant。, created_atdatetime(2024, 1, 15, tzinfotimezone.utc), lessons_learned复用已有基础设施在前期的吸引力会被中后期的 维护成本反噬。如果预期数据量会持续增长 建议一开始就选择有长期扩展能力的方案。, ) ) # 决策3: Embedding模型选择 decisions.append( ArchitectureDecisionRecord( adr_id003, titleEmbedding模型选型, statusDecisionStatus.ACCEPTED, context需要选择文本向量化模型。主要场景中文商品描述和用户问题的语义匹配。 预算有限优先考虑API调用模式不自己部署模型。, decision选择OpenAI text-embedding-3-small 综合考虑性价比和中文表现。, options_considered[ Option( nameOpenAI text-embedding-3-small, description1536维$0.02/1M tokens, pros[ 中文效果优秀, API稳定SLA有保障, 成本可控, ], cons[ 网络延迟海外API, 数据出境合规风险, 供应商锁定, ], scores{中文效果: 9, 成本: 8, 合规: 6}, ), Option( nameBGE-M3 (本地部署), descriptionBAAI开源1024维, pros[ 数据不出境合规性好, 无API调用成本, 可微调, ], cons[ 需要GPU服务器运维成本, 冷启动慢, 需要工程团队有模型部署经验, ], scores{中文效果: 8, 成本: 7, 合规: 10}, ), ], factors[ DecisionFactor(中文效果, 0.35, 在中文语义匹配上的表现), DecisionFactor(成本, 0.35, 综合考虑API费用和运维成本), DecisionFactor(合规, 0.3, 数据安全和隐私合规), ], consequencesOpenAI Embedding方案在中文效果上达到预期 但网络延迟平均增加50ms对整体延迟有一定影响。 合规方面暂未遇到问题但如果是金融/医疗场景应当优先BGE。, created_atdatetime(2024, 1, 20, tzinfotimezone.utc), lessons_learnedEmbedding模型选择要与业务场景的合规要求对齐。 通用场景选API方案省心敏感行业选本地部署稳妥。, ) ) # 决策4: 多Agent协作模式 decisions.append( ArchitectureDecisionRecord( adr_id004, title多Agent协作模式, statusDecisionStatus.ACCEPTED, context客服场景需要多个Agent协作意图识别Agent、知识检索Agent、 工具调用Agent、转人工决策Agent。问题是这些Agent是独立的还是由 一个Supervisor统一调度, decision采用Supervisor模式一个主Agent 根据用户意图动态路由到子Agent。, options_considered[ Option( nameSupervisor路由, description一个主Agent负责任务分发和结果汇总, pros[ 中心化调度状态管理简单, 适合有明显意图分类的场景, 子Agent可以独立开发和测试, ], cons[ Supervisor是单点瓶颈, 路由决策错误会导致整体失败, ], scores{可控性: 9, 扩展性: 7, 容错性: 5}, ), Option( nameAgent协作网络, description多个Agent通过消息传递自主协商, pros[ 去中心化单个Agent故障不影响整体, 适合复杂多步骤任务, ], cons[ 实现复杂度高, 调试困难消息链路长, 对于客服场景过度设计, ], scores{可控性: 4, 扩展性: 9, 容错性: 8}, ), ], factors[ DecisionFactor(可控性, 0.4, 客服场景需要确定性的行为), DecisionFactor(扩展性, 0.3, 未来可能增加新的Agent角色), DecisionFactor(容错性, 0.3, 单个Agent失败后的恢复能力), ], consequencesSupervisor模式在客服场景运行良好。 路由准确率达到93%。但如果遇到用户意图模糊比如同时问订单和优惠 Supervisor有时会选择错误的子Agent。 计划加入意图澄清环节而不是直接路由。, created_atdatetime(2024, 2, 5, tzinfotimezone.utc), lessons_learnedAgent协作模式要先看业务场景的复杂度。 简单场景用Supervisor足够复杂场景再考虑去中心化。 不要为了解决未来的问题而过度设计。, ) ) # 决策5: 异步处理选型 decisions.append( ArchitectureDecisionRecord( adr_id005, title异步任务队列选型, statusDecisionStatus.ACCEPTED, context文档上传后需要异步处理chunk切分→Embedding生成→向量索引写入。 这个流程耗时较长大文档可能需要几分钟不能阻塞API响应。, decision选择Celery Redis因为团队已有Redis Celery是Python生态最成熟的异步任务队列。, options_considered[ Option( nameCelery Redis, descriptionPython经典异步任务队列, pros[ 生态成熟文档丰富, 团队有使用经验, 支持任务重试和监控, ], cons[ 配置复杂broker、backend、worker, Redis做broker在大量任务时有丢消息风险, 任务追踪需要额外工具Flower, ], scores{成熟度: 9, 运维复杂度: 6, 可靠性: 7}, ), Option( nameRQ (Redis Queue), description轻量级Redis任务队列, pros[极其简单几行代码就能用, 部署依赖少], cons[ 功能有限无任务链、无优先级, 社区活跃度低于Celery, ], scores{成熟度: 6, 运维复杂度: 9, 可靠性: 6}, ), ], factors[ DecisionFactor(成熟度, 0.4, 生产环境的稳定性), DecisionFactor(运维复杂度, 0.3, 长期维护成本), DecisionFactor(可靠性, 0.3, 任务不丢失、不重复), ], consequencesCelery在稳定运行6个月后出现了一次broker连接断开 导致任务积压的问题。排查后发现是Redis连接池没有配置心跳。 总体满足需求但配置项确实太多新手容易踩坑。, created_atdatetime(2024, 2, 10, tzinfotimezone.utc), lessons_learnedCelery功能强大但需要投入时间做好生产级配置。 小团队如果任务简单不需要链式/分组RQ可能是更好的选择。, ) ) # 决策6: 日志与可观测性 decisions.append( ArchitectureDecisionRecord( adr_id006, title日志与可观测性方案, statusDecisionStatus.ACCEPTED, contextLLM应用的调试和传统应用不同——你不仅要看哪个函数报错了 还要看LLM在推理时看到了什么上下文、生成了什么内容、花费了多少token。 需要一个能追踪LLM调用链路的可观测性方案。, decision结构化日志(JSON格式) OpenTelemetry trace Grafana可视化。, options_considered[ Option( nameLangSmith, descriptionLangChain官方可观测性平台, pros[ 开箱即用的LLM调用追踪, Prompt版本管理和A/B测试, 和LangChain深度集成, ], cons[ 付费$39/月, 数据存在于外部平台合规问题, 供应商锁定, ], scores{易用性: 9, 成本: 5, 灵活性: 6}, ), Option( name自建(OTel Grafana), description基于开源方案搭建可观测性, pros[ 数据完全自控, 无持续费用, 可定制化程度高, ], cons[ 需要搭建和维护基础设施, LLM特定的trace能力需自己实现, 初期投入较大, ], scores{易用性: 6, 成本: 8, 灵活性: 9}, ), ], factors[ DecisionFactor(易用性, 0.25, 开发和调试效率), DecisionFactor(成本, 0.35, 长期运营成本), DecisionFactor(灵活性, 0.4, 定制化需求满足能力), ], consequences自建方案初期投入了约2周搭建基础设施 但长期来看节省了费用且数据完全自主可控。 缺点是缺少LangSmith那种一键看LLM输入输出的便捷体验。 团队花了额外时间自建了LLM trace格式规范。, created_atdatetime(2024, 3, 1, tzinfotimezone.utc), lessons_learned可观测性不是一个选A还是选B的问题 而是先跑通基本监控日志指标再逐步叠加高级能力。 不要一上来就追求完美。, ) ) return decisions async def main(): manager ADRManager() decisions create_six_real_decisions() for adr in decisions: filepath await manager.record_decision(adr) print(f决策记录已保存: {filepath}) print(f\n共记录 {len(decisions)} 个技术决策) # 模拟复盘 await manager.review_decision( decisions[1], # ADR-002 向量数据库 lessonsRedis OOM事件证明向量数据和缓存在同一实例是高风险架构。 如果当前QPS继续增长建议在下个迭代完成迁移。, new_statusDecisionStatus.SUPERSEDED, ) print(\n已完成对 ADR-002 的复盘) if __name__ __main__: asyncio.run(main())边界权衡决策记录的粒度不是每个技术选择都值得写ADR。我们的标准是如果一个决策满足以下任一条件就写ADR——影响两个以上模块、在多个可行方案中纠结过、有长期维护成本、或者未来可能会被质疑。我们项目里30个关键决策中最终给6个写了ADR。**决策的后悔成本**应该纳入考量的因素权重。比如向量数据库选错了迁移成本是数据导出→新库导入→切换流量大概一周的工作量但Agent框架选错了迁移成本是重写所有业务逻辑可能要一个月。后悔成本越高的决策越应该花时间调研和论证。**够用就好vs一步到位**是一个反复出现的trade-off。我们的策略是如果未来6个月内确定性高数据量增长可预测、需求方向明确就用够用就好如果不确定性高业务方向可能调整、数据量可能暴涨就留够扩展空间。ADR-002Redis早期够用但后来不够了就是典型的6个月后需求变了。团队规模的考量在每次决策中都不一样。3人小团队Celery的配置复杂度是减分项10人团队有专门的SRE后同样的复杂度就不是问题了。技术选型决策中团队能力这个因素不是静态的每半年要重新评估一次。五、总结技术选型复盘下来最大的感悟是好决策和坯决策的区别不在结果在过程。一个决策即使事后证明选错了如果当时有清晰的理由、完整的记录、明确的触发条件什么情况下需要重新评估那它也是一个好决策——因为它为后续调整提供了充分的上下文。这6个决策里2个LangChain、Celery证明选择正确且仍在运行2个Redis向量搜索、Embedding服务需要调整方向2个多Agent模式、可观测性在不同规模下可能需要不同方案。每个决策的当时理由比最终结果更有分享价值——因为你的项目约束和我的不一样但决策的方法论是通用的。我强烈建议每个项目用ADR记录关键决策。不一定要用我们上面的代码工具一个markdown文件就够。重要的是养成习惯每次做技术选型花30分钟写清楚问题是什么、有哪些选项、为什么选这个、什么情况下要重新考虑。这30分钟可能在未来半年为你节省30小时的排查和争论。