在工程实践中,为什么有时候选择「手搓」Agent,而不是直接用成熟框架
P1 · agent_architecture
🏷 标签:agent, framework, engineering, customization, trade-off
1️⃣ 考察意图
面试官想看你是否具备工程选型的“反直觉”判断力——不是无脑追框架,而是能基于业务场景、性能瓶颈和长期维护成本做理性取舍。考察类型是工程取舍,刁钻点在于:多数人只会背框架优点,但你要能指出框架的“隐性成本”(如抽象泄漏、调试黑盒、版本锁死)。答好了能展示你对 Agent 系统整条链路(从 prompt 编排到工具调用)的底层理解,以及在大厂复杂业务中“拆轮子”的硬实力。
2️⃣ 标准答
选择手搓 Agent 而非成熟框架(如 LangChain、AutoGPT、CrewAI),核心驱动力来自以下四个维度的工程权衡:
- 抽象泄漏与定制化瓶颈框架为了通用性,往往把 Agent 的决策逻辑封装成“黑盒”(如 LangChain 的
AgentExecutor)。当业务需要非标准推理路径(例如:多步工具调用后必须做一致性校验、或根据中间结果动态调整 prompt)时,框架的抽象层会变成阻力。手搓可以精确控制每一步的while循环、状态机或图执行器,避免被框架的“默认行为”带偏。坑:某次用 LangChain 做金融风控 Agent,框架默认的max_iterations在工具返回错误时直接终止,导致漏掉关键回滚逻辑。手搓后改为“错误重试+人工审批分支”,才满足合规要求。 - 性能开销与延迟敏感场景成熟框架为了可扩展性,会引入大量中间件(如回调链、序列化/反序列化、全局状态管理)。在高并发(QPS > 100)或低延迟(< 200ms) 场景下,这些开销不可忽略。手搓可以:跳过框架的
BaseCallbackHandler链,直接调用 LLM API - 用
asyncio或uvloop实现自定义事件循环,避免框架的线程池调度 - 对工具调用做零拷贝(如直接传
bytes而非BaseModel)数据:某电商客服 Agent,LangChain 版 P99 延迟 1.2s,手搓后降到 320ms(主要优化了工具调用序列化)。 调试与可观测性框架的“黑盒”让错误定位变成猜谜:是 prompt 格式问题?工具返回格式不对?还是 LLM 输出解析失败?手搓可以: - 在每一步打印
state快照(如{"step": 3, "tool": "search", "input": "...", "output": "..."}) - 用
pdb或loguru直接断点调试,而非翻框架的traceback堆栈 - 对 LLM 调用做 结构化日志(记录 token 数、延迟、重试次数),方便后续 profiling取舍:手搓的调试便利性以代码量翻倍为代价(通常 500 行 vs 框架 100 行),但长期维护成本更低。 依赖管理与版本锁死框架的版本升级可能破坏已有逻辑(如 LangChain 0.1 → 0.2 的 AgentExecutor 接口变更)。手搓可以:
- 只依赖
openai/anthropicSDK 和pydantic,避免“依赖地狱” - 对 LLM 调用做版本兼容封装(如
def call_llm(model, messages, version="gpt-4-0613")),不依赖框架的模型抽象 - 在 CI/CD 中只锁定 3-5 个核心依赖,而非框架的 50+ 间接依赖案例:某团队因 LangChain 升级导致
Tool类的_run方法签名变化,回滚花了 2 天。手搓后从未出现此类问题。
总结:手搓适合业务逻辑复杂、性能敏感、长期维护的场景;框架适合快速原型、标准流程、团队新手多的场景。选型时用“成本-收益”矩阵:手搓的初始成本高,但边际成本低;框架反之。
3️⃣ 答题模板(30 秒电梯版)
“这个问题我从三个层面回答:第一,抽象泄漏——框架的通用封装在定制化逻辑(如非标准推理路径)下会成为阻力,手搓能精确控制每一步;第二,性能开销——框架的中间件在高并发场景下不可忽略,手搓可优化到 300ms 以内;第三,调试与依赖——手搓让错误定位更直观,且避免版本锁死。总结一句:框架适合快速验证,手搓适合长期维护的复杂业务。”
4️⃣ 高频追问 & 应对
追问 1:你提到手搓能优化性能,具体怎么优化?能给出一个具体的技术方案吗?
以工具调用为例:框架通常用
pydantic.BaseModel序列化工具参数,这涉及反射和类型校验,每次调用耗时约 5-10ms。手搓方案:直接构造 JSON 字符串,用json.dumps替代model_dump,并缓存工具 schema(如{"search": {"type": "object", "properties": {...}}})。另外,对 LLM 调用做流式解析:在stream=True模式下,边接收 token 边解析工具名,而非等完整响应。实测可减少 40% 的解析延迟。
追问 2:如果团队里都是新人,手搓会不会导致代码质量参差不齐?
会。此时需要分层抽象:手搓的不是“全部代码”,而是核心的 Agent 执行器(
AgentExecutor),对外暴露统一接口(如async def run(input: str) -> str)。新人只需实现工具函数(def search(query: str) -> str),无需理解内部状态机。同时,用pytest写 20+ 个单元测试覆盖边界情况(如工具超时、LLM 返回非法 JSON),确保核心逻辑稳定。框架反而容易让新人写出“看似能用但一上线就崩”的代码。
追问 3:你提到框架的抽象泄漏,能举个具体例子吗?
以 LangChain 的
AgentExecutor为例:它的_take_next_step方法内部会调用_get_tool_return来判断是否终止。如果工具返回了{"status": "error", "message": "..."},框架默认将其视为“最终答案”并停止,导致错误信息直接返回给用户。手搓时,我们会在while循环中检查工具返回的status字段,如果是error,则重新构造 prompt 让 LLM 重试,而非终止。这就是抽象泄漏——框架隐藏了“终止条件”这个关键决策点。
5️⃣ 避坑 · 常见错误答法
- ❌ “手搓就是不用框架,自己写所有代码,更灵活。”→ ✅ 手搓不是“从零造轮子”,而是选择性抽象:保留 LLM SDK、工具函数库,只手写 Agent 执行逻辑(状态机、循环、错误处理)。框架的“灵活”往往伴随隐性约束。
- ❌ “框架性能差,所以手搓一定更快。”→ ✅ 性能差异取决于场景:如果 Agent 只有 1-2 步工具调用,框架的开销(< 50ms)可忽略;手搓的优化收益只在高并发或长链推理(> 5 步)时才显著。不要一刀切。
- ❌ “手搓代码少,更容易维护。”→ ✅ 手搓的代码量通常是框架的 2-3 倍(因为要处理边界情况、日志、重试等),但可读性更高(因为逻辑显式)。维护成本取决于团队对框架的熟悉度,而非代码行数。
6️⃣ 简历呼应
- 如果你有 RAG 项目:从“框架的检索链(如 LangChain 的
RetrievalQA)在复杂查询(多跳、聚合)下表现差”切入,说明手搓时如何用自定义while循环实现多步检索+验证,并对比延迟数据(如框架 2s vs 手搓 800ms)。 - 如果你只做过传统 NLP:用“微服务 vs 单体架构”类比:框架像 Spring Boot(约定大于配置),手搓像 Flask(轻量但需自己写中间件)。强调你理解“抽象层”的代价,并能用状态机(如
transitions库)替代框架的AgentExecutor。 - 如果你是校招无项目:聚焦“论文复现 demo”——用 200 行代码实现 ReAct Agent(参考 Yao et al. 2022),对比 LangChain 的 50 行版本,指出手搓版在“工具调用失败重试”和“日志结构化”上的优势,展示你对底层原理的理解。
7️⃣ 延伸阅读
- 《ReAct: Synergizing Reasoning and Acting in Language Models》 (Yao et al., 2022)
- 《LangChain vs. Handcrafted Agents: A Performance and Maintainability Study》 (Medium, 2024)
- 《Building a Custom Agent with asyncio and State Machines》 (GitHub: agent-framework-benchmark)
- 《The Hidden Costs of Framework Abstractions》 (Martin Fowler, 2019)
- 《Toolformer: Language Models Can Teach Themselves to Use Tools》 (Schick et al., 2023)