1. 这不是工具选型指南,而是一份“踩坑现场直播”实录

你打开终端,敲下 pip install ,心里想的是“今天终于能把RAG系统跑通”,结果三分钟后,你盯着满屏的依赖冲突报错发呆—— llama-index 要求 pydantic<2.0 langchain 刚升级到 v0.1.0 又强制要求 pydantic>=2.5 ,而你本地那个 smolagent 的 demo 脚本,连 requirements.txt 都没写全,只有一行 # TODO: add deps 。这不是段子,是我上个月在客户现场真实发生的第7次崩溃。LlamaIndex、LangChain、Hugging Face smolagent——这三个名字现在几乎出现在每一份AI工程岗JD里,但没人告诉你:它们根本不是同一类东西,强行放在一起比“谁更好用”,就像问“螺丝刀、电钻和装修合同模板,哪个更适合拧紧一颗M4螺栓”。LlamaIndex 是 数据管道编排器 ,它专注把非结构化文档切片、嵌入、索引、召回这一整条链路打磨到工业级稳定;LangChain 是 LLM应用胶水层 ,它的核心价值在于抽象出 Chain Agent Tool 这些模式,让你能像搭乐高一样组合大模型能力;而 Hugging Face smolagent 是一个 极简主义实验框架 ,它甚至不提供向量数据库集成,所有状态都靠 Python 字典硬编码,目的就一个:让你30秒内看到 agent loop 跑起来,而不是花两小时配好 ChromaDB。我见过太多团队,因为没分清这三者的定位边界,硬把 LlamaIndex 当成 agent 框架来写 memory 管理逻辑,结果在 QueryEngine 里塞了17个自定义 callback,最后发现性能瓶颈根本不在 embedding 模型,而在自己写的 AsyncCallbackHandler 里一个没加 await print() 。这篇文章不给你列参数对比表,也不做“综合评分”,我会带着你重走一遍我亲手搭建三个真实场景的全过程:一个需要毫秒级响应的客服知识库(LlamaIndex 主力)、一个要调用5个内部API并做决策的运维助手(LangChain 主力)、一个给实习生练手的本地PDF问答玩具(smolagent 主力)。每个场景都会暴露它们最真实的脾气——比如 LlamaIndex 的 SimpleVectorStore 在并发查询时默认不加锁,LangChain 的 ReAct agent 会因为 prompt 中一个标点符号错误就无限循环,smolagent 的 run_step() 方法居然不校验 tool 返回值类型……这些细节,文档里不会写,但线上故障单上全是。

2. 核心定位解构:为什么它们根本不在一个维度上打架

2.1 LlamaIndex:数据中枢,不是应用框架

LlamaIndex 的设计哲学非常直白: 让数据说话,而不是让代码说话 。它的整个架构围绕一个核心假设展开——90% 的 RAG 效果瓶颈,不在 LLM 本身,而在数据预处理与检索质量。所以你看它的 API, Document Node Index QueryEngine ,全是数据实体;它的核心模块 IngestionPipeline 里, TransformComponent 支持自定义文本清洗、 EmbeddingModel 接口强制要求实现 get_text_embedding_batch ,连 VectorStore 抽象层都预留了 delete_nodes persist 的钩子。这不是巧合,这是刻意为之。我去年帮一家医疗SaaS公司重构知识库,他们原来的方案是 LangChain + ChromaDB,所有 PDF 解析逻辑全写在 load_and_split 函数里,结果某天上传了一份扫描版CT报告PDF,OCR识别出“肺部阴影”,但解析器把它当成了普通文字节点,直接喂给了 embedding 模型,最终在用户问“这个阴影是良性还是恶性”时,系统从索引里召回了10页无关的放射科术语解释。换成 LlamaIndex 后,我们只改了三处:第一,在 IngestionPipeline 里插入一个 ImageTextExtractor 组件,专门处理 PDF 中的图片区域;第二,用 MetadataMode.ALL Node 保留原始页面坐标和置信度;第三,在 QueryEngine response_mode="tree_summarize" 下,强制要求 summary 模板必须包含 source_page: {node.metadata["page"]} 。上线后,同类问题投诉下降了83%。关键点在于:LlamaIndex 不阻止你写业务逻辑,但它把数据质量控制点变成了可插拔的组件,而不是藏在 if-else 里的魔法字符串。它的“不好用”,恰恰体现在你试图用它写 agent memory 时——它没有 MemoryBuffer 类,没有 ConversationSummaryBufferMemory ,你得自己用 VectorStoreIndex 存历史对话,再手动注入 system_prompt ,这种“反模式”的痛苦,其实是它在提醒你:“你的问题,不该在这里解决”。

2.2 LangChain:应用模式引擎,不是数据管道

LangChain 的灵魂是 Runnable 协议。从 LLM Tool ,从 Chain AgentExecutor ,所有对象都必须实现 ainvoke() 方法,这背后是一套严格的异步执行契约。它的强大,体现在对“应用模式”的抽象能力上。比如 ReAct agent,它不是简单地把用户问题丢给 LLM,而是严格遵循“Thought-Action-Observation”三步循环:先生成 Thought (推理当前该做什么),再输出 Action (调用哪个 tool),最后等待 Observation (tool 返回结果)后再进入下一步。我在做银行风控助手时,用 ReAct 实现了一个“贷款申请合规性检查”流程:第一步 Thought 是“需要验证申请人身份证号是否在黑名单”, Action 调用 BlacklistCheckerTool ;第二步 Observation 返回 {"status": "found", "reason": "涉诉未结"} Thought 立刻转向“需检查抵押物估值”, Action 切换到 AppraisalTool 。这种状态机式的控制流,LangChain 用 AgentExecutor 就能封装掉90%的胶水代码。但它的代价也很明显: 所有灵活性都建立在 prompt 工程之上 ReAct 的 system prompt 里有一行 You have access to the following tools: ,如果这里漏写了一个 tool name,或者 tool description 里用了中文顿号“、”而不是英文逗号“,”,agent 就会卡死在 Thought 阶段,不断输出 I need to check... 却永远不触发 Action 。我调试过整整两天,最后发现是 Tool description 字段里写了“用于查询用户余额、信用分”,那个中文顿号让 LLM 的 parser 直接崩溃。LangChain 的“易用”,本质是把 prompt 复杂度封装进了 AgentType 枚举里,但当你需要微调行为时,就得钻进 ReActOutputParser 源码里改正则表达式。它不提供向量数据库,不是因为它做不到,而是它认为“数据存储”属于基础设施层,应该由 LlamaIndex 或直接调用 ChromaDB SDK 来完成——这种分层思想,让它在复杂 agent 场景中如鱼得水,但在纯文档检索场景里,反而显得笨重。

2.3 Hugging Face smolagent:教学沙盒,不是生产框架

smolagent 的 GitHub README 第一行就写着:“A tiny, educational agent framework.” 它的代码只有不到200行,核心就两个类: SmolAgent Tool SmolAgent.run() 方法里, while True: 循环体只有6行: thought = self.llm.invoke(thought_prompt) action = parse_action(thought) observation = tool.run(action_input) history.append(...) if "FINISH" in thought: break 。没有异步,没有重试,没有超时控制, Tool.run() 方法甚至直接 return eval(input_str) (官方 demo 里真这么写的!)。它的存在意义,是让一个刚学完 Python 基础的实习生,能在15分钟内理解 agent 的基本工作流。我带过一个暑期实习项目,任务是做一个本地PDF问答工具。如果用 LangChain,光是配置 RecursiveCharacterTextSplitter chunk_size=512 chunk_overlap=128 就要讲半小时;用 LlamaIndex,得先解释 ServiceContext StorageContext 的区别。而 smolagent,我只给了实习生三行代码:

class PDFTool(Tool):
    def run(self, query: str) -> str:
        # 这里用 PyPDF2 提取文本,用 sentence-transformers 做相似度匹配
        return "答案来自第3页:..."
agent = SmolAgent(llm=Ollama(model="llama3"), tools=[PDFTool()])
print(agent.run("这份PDF讲了什么?"))

他当天下午就跑通了。但第二天,当他想加个“如果没找到答案就联网搜索”的功能时,问题来了:smolagent 的 Tool 没有 is_async 属性, run() 方法是同步阻塞的,一旦 requests.get() 超时,整个 agent 就卡死。这时候,框架的“教育性”就变成了“生产障碍”——它不提供解决方案,只提供思考起点。它的价值,不在于帮你建系统,而在于逼你思考:当去掉所有封装,agent 最小可行单元到底是什么? Thought 的格式约束是否必要? Observation 的返回值类型该如何定义?这些问题的答案,才是你在 LangChain 或 LlamaIndex 里写 CustomOutputParser 时真正需要的底层认知。

3. 实操场景拆解:三个真实项目,暴露它们的“真面目”

3.1 场景一:金融客服知识库(LlamaIndex 主力,LangChain 辅助)

需求 :某券商APP的在线客服,需支持用户用自然语言查询“创业板开户条件”“融资融券利率调整”等政策问题,响应时间 < 800ms,准确率 > 95%。
技术选型逻辑 :政策文档更新频繁(每周至少3次),且含大量表格、条款编号、PDF 扫描件,数据质量是生死线。
LlamaIndex 实操细节

  • 文档解析层 :不用 SimpleDirectoryReader ,改用自定义 PolicyPDFReader ,继承 BaseReader ,重写 load_data() 。关键点在于:对 PDF 中的表格区域,调用 pdfplumber 提取 table.extract() 后,将每行转为 Node 时, metadata 强制添加 "table_row_index": i "has_header": True 。这样在 QueryEngine response_mode="compact" 下,summary 模板能精准引用 row {node.metadata["table_row_index"]}
  • 索引构建层 :放弃默认的 VectorStoreIndex ,改用 SummaryIndex + VectorStoreIndex 双索引。 SummaryIndex 存储每份政策文件的摘要(用 LLM 生成), VectorStoreIndex 存储细粒度文本块。查询时,先用 SummaryIndex 快速定位相关文件( index.as_query_engine().query("关于融资融券的政策") ),再用 VectorStoreIndex 在该文件内做精确检索。实测将平均召回延迟从 1200ms 降至 430ms。
  • 检索增强层 QueryEngine similarity_top_k=3 是陷阱。我们发现,当用户问“开通创业板需要多少钱”,top3 可能包含“资金要求”“交易经验要求”“风险测评要求”三个不同节点,但 LLM 需要的是整合信息。于是改用 SubQuestionQueryEngine ,让它自动拆解为 ["创业板开通的资金门槛是多少?", "是否有最低资产要求?"] ,再并行查询。
    LangChain 辅助点 :只用 PromptTemplate StringOutputParser 封装最终回答生成。 prompt 模板里明确要求:“仅基于以下检索结果回答,禁止编造。若结果中无明确数字,回答‘根据最新政策,具体金额请咨询营业部’。” 这里 LangChain 的价值是提供标准化的 prompt 编排,而非 agent 控制流。

提示:LlamaIndex 的 Node 对象有 get_content() get_metadata_str() 两个方法,很多人直接拼接 node.text + node.metadata 导致 embedding 向量污染。正确做法是用 node.get_content(metadata_mode=MetadataMode.EMBED) ,它会自动过滤掉不适合 embedding 的元数据字段。

3.2 场景二:IT运维智能助手(LangChain 主力,LlamaIndex 辅助)

需求 :某云服务商的内部运维平台,需支持工程师输入“查看华东1区所有MySQL实例的CPU使用率”,agent 自动调用监控API、数据库API、告警API,聚合结果后生成自然语言报告,并在异常时触发工单。
技术选型逻辑 :动作链路长(平均5步),涉及多系统权限认证、异步任务调度、失败回滚,需要强状态管理。
LangChain 实操细节

  • Tool 设计 :每个 API 封装为独立 Tool ,但关键在 args_schema 。例如 GetMySQLMetricsTool args_schema 不是简单 dict ,而是继承 BaseModel
class GetMySQLMetricsInput(BaseModel):
    region: str = Field(description="地域代码,如 'cn-shanghai'")
    instance_ids: List[str] = Field(description="实例ID列表,最多20个")
    time_range: str = Field(description="时间范围,格式 'last_5m' 或 '2024-01-01T00:00:00Z/2024-01-01T01:00:00Z'")

这样 ReAct agent 在生成 Action Input 时,会严格按 JSON Schema 校验,避免传入 "region": "shanghai" 这种非法值。

  • Agent 类型选择 :不用默认 ReAct ,改用 OpenAIFunctionsAgent (即使不用 OpenAI,也用其协议)。因为它的 functions 参数支持 {"name": "get_metrics", "parameters": {...}} ,比 ReAct 的字符串解析更健壮。我们甚至写了 CustomFunctionTool ,在 invoke() 里加入 try-except ,捕获 requests.Timeout 后返回 {"error": "API timeout, retrying..."} ,agent 会自动重试。
  • Memory 管理 :用 ConversationBufferWindowMemory ,但 k=5 是红线。测试发现,当对话历史超过7轮, ReAct Thought 会开始混淆上下文。解决方案是:在 AgentExecutor handle_parsing_errors 回调里,检测到 parsing_error 时,主动清空 memory.buffer 并重置 agent_state
    LlamaIndex 辅助点 :只用 VectorStoreIndex 存储运维手册PDF,作为 Tool 的 fallback。当 GetMySQLMetricsTool 返回空数据时,agent 触发 SearchManualTool ,用 index.as_query_engine().query() 查找“MySQL监控指标说明”。这里 LlamaIndex 是纯数据源,不参与控制流。

注意:LangChain 的 AgentExecutor 默认 max_iterations=15 ,但在生产环境必须设为 5 。我们遇到过一次事故:某个 Tool Observation 返回了超长日志(12MB), ReAct agent 试图将其全部塞进 prompt,导致 token 超限, max_iterations 被耗尽后抛出 AgentFinish 异常,但 agent 已经执行了3次无效 API 调用,造成监控接口被限流。

3.3 场景三:实习生PDF问答玩具(smolagent 主力,零外部依赖)

需求 :给5名实习生分配任务,用本地部署的 llama3:8b 模型,实现一个能读取指定PDF文件并回答问题的命令行工具,2天内交付,代码不超过100行。
技术选型逻辑 :目标不是建系统,是建立对 agent 工作流的肌肉记忆。
smolagent 实操细节

  • 极简 Tool 实现 :不碰 requests 或数据库,只用标准库。 PDFTool.run() 方法核心就三行:
def run(self, query: str) -> str:
    text = extract_text_from_pdf(self.pdf_path)  # PyPDF2
    chunks = [text[i:i+512] for i in range(0, len(text), 256)]
    scores = [cosine_similarity(embed(query), embed(chunk)) for chunk in chunks]
    best_chunk = chunks[scores.index(max(scores))]
    return f"根据文档:{best_chunk[:200]}..."
  • LLM 封装 :不用 langchain_community.llms.Ollama ,直接用 ollama.generate() 的同步 API。 SmolAgent.llm.invoke() 方法里, prompt 就是硬编码的字符串:
prompt = f"""你是一个PDF问答助手。
文档内容:{self.context}
用户问题:{query}
请直接回答,不要解释过程。"""
  • 致命陷阱规避 :smolagent 的 run_step() 方法默认不校验 Tool 返回值。我们加了一行 assert isinstance(observation, str), f"Tool returned {type(observation)}" ,并在 except AssertionError 里打印完整 traceback。结果第一天就发现: PyPDF2 解析某些扫描PDF时返回 None cosine_similarity TypeError ,但 agent 仍继续循环。加了断言后,实习生立刻定位到 extract_text_from_pdf() 需要 fallback 到 pytesseract
    为什么不用 LangChain/LlamaIndex :实习生反馈,LangChain 的 Document 类有 metadata excluded_llm_metadata_keys 等12个属性,看源码花了3小时;LlamaIndex 的 StorageContext 需要 chromadb ,但 pip install chromadb 在 Windows 上编译失败,折腾了一整天。smolagent 的“简陋”,在这里成了最大优势——它强迫你直面问题本质:PDF 文本提取、向量相似度计算、prompt 格式控制。当这些基础能力内化后,再学 LangChain 的 RetrievalQA 或 LlamaIndex 的 QueryEngine ,就不再是记 API,而是理解设计意图。

实操心得:smolagent 的 SmolAgent 类里, self.history List[Dict] ,但官方 demo 用 str(history) 打印,导致中文乱码。正确做法是 json.dumps(history, ensure_ascii=False, indent=2) 。这个细节,暴露了框架的“教学”属性——它不处理工程细节,只留给你填坑空间。

4. 工具链深度对比:参数、性能、扩展性的真实数据

4.1 核心能力矩阵(基于 v0.10.42 / v0.1.23 / v0.2.0 版本实测)

能力维度 LlamaIndex LangChain Hugging Face smolagent
向量数据库支持 内置 ChromaDB、Qdrant、Weaviate、PGVector, VectorStoreIndex 抽象层统一接口 仅通过 langchain_community.vectorstores 模块桥接,需手动 import 无内置支持,需自行实现 Tool 调用
文档解析能力 IngestionPipeline 支持自定义 TransformComponent ,可插拔 OCR、表格提取、代码块分离 DocumentLoaders 种类多(50+),但解析逻辑耦合在 loader 内,修改需 fork 无解析能力,完全依赖用户实现 Tool
Agent 控制流 无原生 agent,需组合 QueryEngine + LLM 手写循环 ReAct / Plan-and-Execute / OpenAIFunctions 多种 agent type, AgentExecutor 封装重试/超时 SmolAgent.run() 为固定 while 循环,不可定制步骤数或条件
异步支持 QueryEngine.aquery() 全链路异步, IngestionPipeline.arun() 支持并发解析 Runnable 协议强制 ainvoke() AgentExecutor 支持 atransform() 流式处理 同步阻塞, run() 方法无 async 版本
内存管理 SimpleVectorStore 默认不持久化, persist() 需显式调用,无自动 GC ConversationBufferMemory 等提供 clear() ,但无向量内存自动清理机制 self.history 为纯 Python list,无大小限制,OOM 风险高
错误处理 QueryEngine VectorStoreQueryError ,可捕获后降级为关键词检索 AgentExecutor handle_parsing_errors 可自定义,但需手动处理 LLMGenerationError 无错误处理, run() 中任何异常都会中断整个 agent

4.2 性能基准测试(硬件:MacBook Pro M2 Max, 64GB RAM)

测试场景:1000页PDF(含表格、图片),embedding 模型 BAAI/bge-small-en-v1.5 ,查询 “What is the policy on data retention?”

指标 LlamaIndex (VectorStoreIndex) LangChain (ChromaDB + RetrievalQA) smolagent (自定义 Tool)
索引构建时间 42.3s 58.7s N/A(无索引)
单次查询延迟 312ms 489ms 1.2s(纯 CPU 计算)
内存占用峰值 1.8GB 2.3GB 450MB
并发 QPS(10线程) 12.4 8.7 3.1
准确率(Top1召回) 96.2% 89.5% 73.8%

关键发现

  • LlamaIndex 的延迟优势来自 VectorStoreIndex similarity_top_k 优化——它在 ChromaDB 底层调用 query_embeddings 时,启用了 n_results=3 的批处理,而 LangChain 的 RetrievalQA 默认逐个查询。
  • smolagent 的准确率低,不是因为算法差,而是 cosine_similarity 计算时未归一化向量, embed(query) embed(chunk) 的 L2 norm 不一致,导致相似度失真。修复后准确率升至 88.1%,但延迟增至 1.8s。
  • LangChain 内存占用高,源于 RetrievalQA combine_documents_chain 会将所有召回 Document 加载进内存,再拼接成 prompt。LlamaIndex 的 response_mode="tree_summarize" 则分块处理,内存更友好。

4.3 扩展性实战:当需求升级时,谁更容易“长大”

需求升级1:支持多文档交叉引用

  • LlamaIndex:只需在 IngestionPipeline 中添加 CrossDocumentLinker 组件,它会自动分析 Node 间的语义关联,生成 relationships 字段。查询时 QueryEngine 可启用 recursive_retrieval=True
  • LangChain:需重写 RetrievalQA combine_documents_chain ,手动实现文档间关系图谱,代码量增加300+行。
  • smolagent:无法扩展, Tool run() 方法只能处理单个 PDF,要支持多文档需重写整个 agent 循环。

需求升级2:添加人工审核环节

  • LlamaIndex:在 QueryEngine response_synthesizer 中插入 HumanFeedbackSynthesizer ,当 confidence_score < 0.7 时,返回 {"status": "pending_review", "suggestion": "建议联系法务部确认"}
  • LangChain:用 RouterChain ,配置 condition lambda x: x["confidence"] < 0.7 ,路由到 HumanReviewChain
  • smolagent:需在 run_step() 循环中硬编码 if "pending_review" in observation: input("请人工确认:") ,破坏框架简洁性。

需求升级3:对接企业微信机器人

  • LlamaIndex:作为数据源,提供 query_api() 方法,由企业微信 bot 服务调用。
  • LangChain: AgentExecutor 可直接挂载为 FastAPI endpoint, request body 解析为 input response 返回 JSON。
  • smolagent: SmolAgent.run() 是阻塞调用,需用 threading.Thread 包裹,否则企业微信 webhook 超时。

实测教训:在 LangChain 中, AgentExecutor return_intermediate_steps=True 会显著降低性能(+40% 延迟),因为每一步都要序列化 AgentStep 对象。生产环境务必关闭,用 callbacks 代替。

5. 常见问题与排查技巧实录:那些文档里绝不会写的坑

5.1 LlamaIndex 高频故障与根因分析

问题1: QueryEngine 返回空结果,但文档明明包含关键词

  • 现象 index.as_query_engine().query("创业板开户") 返回 Empty Response ,但用 grep -r "创业板" ./docs/ 能搜到。
  • 根因 SimpleDirectoryReader 默认 filename_as_id=True ,但 Node id_ 字段在 VectorStoreIndex 中不参与 embedding,导致 QueryEngine similarity_top_k 无法匹配。
  • 排查技巧
    1. 先用 index.docstore.docs.values() 打印所有 Node text[:100] ,确认文本已加载;
    2. 再用 index.vector_store.query(...) 直接调用向量库,传入 query_embedding=embed("创业板开户") ,看是否返回空;
    3. 如果向量库返回正常,问题在 QueryEngine response_mode ,尝试 response_mode="no_text" 看是否能拿到 source_nodes
  • 终极解法 :在 IngestionPipeline 中, TransformComponent 添加 id_func=lambda x: hashlib.md5(x.text.encode()).hexdigest() ,确保 Node.id_ 唯一且稳定。

问题2:并发查询时 VectorStoreIndex RuntimeError: dictionary changed size during iteration

  • 现象 :压测时 50 QPS,随机出现 RuntimeError ,日志指向 SimpleVectorStore._data 字典。
  • 根因 SimpleVectorStore _data Dict[str, VectorStoreData] ,默认不加锁,多线程同时 get() add() 会冲突。
  • 避坑方案
    • 生产环境禁用 SimpleVectorStore ,改用 ChromaVectorStore (底层 SQLite 支持并发);
    • 若必须用 SimpleVectorStore ,在 QueryEngine 外层加 threading.Lock() ,但会牺牲性能;
    • 更优雅的解法:用 LlamaIndex StorageContext ,配置 vector_store=ChromaVectorStore(chroma_client=PersistentClient(path="./chroma_db")) ,利用 Chroma 的持久化锁机制。

5.2 LangChain Agent 死循环诊断手册

问题1: ReAct agent 卡在 Thought: I need to... ,永不输出 Action

  • 现象 agent_executor.invoke({"input": "查一下服务器状态"}) 一直等待, timeout=30s 后抛 TimeoutError
  • 根因 ReActOutputParser 的正则 r"Action: ([^\n]*)" 无法匹配 Action: get_server_status ,因为 get_server_status 不在 tools 列表中,或 tool.description 里写了 get_server_status_tool (多了 _tool 后缀)。
  • 快速定位法
    1. 设置 verbose=True ,观察 agent_executor 输出的 prompt ,复制全文到 https://regex101.com/,测试正则匹配;
    2. 检查 tools 列表中的 name 字段,必须与 prompt You have access to the following tools: 下的名称 完全一致 (包括大小写、下划线);
    3. tool_names = [t.name for t in tools] 打印,确认无隐藏空格。
  • 永久修复 :在 ReActOutputParser 初始化时,传入 tool_names=tool_names ,它会自动在正则中加入 | 分隔符。

问题2: AgentExecutor 执行 Action 后, Observation 返回 None ,agent 无限重试

  • 现象 Tool.run() 方法里 print("debug") 有输出,但 agent_executor 日志显示 Observation: None
  • 根因 Tool.run() 方法末尾缺少 return 语句,Python 默认返回 None
  • 防呆设计 :在 Tool 基类中重写 run()
def run(self, *args, **kwargs) -> str:
    result = self._run(*args, **kwargs)
    if result is None:
        raise ValueError(f"Tool {self.name} returned None. Must return str.")
    return str(result)

这样异常会立即抛出,而不是让 agent 在 None 上死循环。

5.3 smolagent 的“教学陷阱”清单

陷阱1: SmolAgent history 不记录 Thought ,只存 Action Observation

  • 后果 :当需要 debug agent 决策路径时,无法回溯 Thought 内容,只能看到 Action: search_pdf ,却不知道为什么选这个 action。
  • 补救措施 :在 run_step() 循环中,手动 self.history.append({"thought": thought, "action": action, "observation": observation}) ,并修改 __str__() 方法打印完整 history。

陷阱2: Ollama 模型的 temperature=0 在 smolagent 中失效

  • 现象 SmolAgent(llm=Ollama(model="llama3", temperature=0)) ,但每次 run() 结果仍不同。
  • 根因 :smolagent 的 llm.invoke() 方法里, kwargs 未透传 temperature ,默认用 ollama.generate() 的全局配置。
  • 硬核修复 :重写 Ollama 类, invoke() 方法中:
def invoke(self, prompt: str, **kwargs) -> str:
    # 合并默认参数和传入参数
    params = {**self.default_params, **kwargs} 
    response = ollama.generate(model=self.model, prompt=prompt, options=params)
    return response["response"]

然后初始化时 Ollama(model="llama3", default_params={"temperature": 0})

陷阱3: PDFTool 在 Windows 上解析中文 PDF 报 UnicodeDecodeError

  • 现象 PyPDF2.PdfReader() 读取含中文的 PDF 时崩溃。
  • 根因 PyPDF2 3.x 版本对中文编码支持弱,需降级到 PyPDF2==2.12.1 ,或改用 pymupdf fitz )。
  • 一键解决 pip uninstall PyPDF2 && pip install PyMuPDF PDFTool.run() 中:
import fitz
doc = fitz.open(self.pdf_path)
text = ""
for page in doc:
    text += page.get_text()

pymupdf 对中文 PDF 的兼容性远超 PyPDF2 ,且速度更快。

6. 我的选型决策树:什么时候该用哪个,以及为什么

我不会再问“LlamaIndex、LangChain、smolagent 哪个更好”,而是问三个问题,答案直接决定技术栈:

第一问:你的核心瓶颈是数据质量,还是应用逻辑?

  • 如果答案是 数据质量 (文档格式混乱、扫描件多、表格密集、更新频繁),选 LlamaIndex 。它的 IngestionPipeline Node 抽象,就是为解决这个问题而生。别被它的“Index”名字迷惑,它本质是数据治理框架。我见过最狠的用法:用 LlamaIndex 解析 10TB 的法律判决书 PDF,自定义 TransformComponent 做案由分类、法条引用抽取、当事人关系图谱构建,最后导出结构化 JSON 供下游系统消费——全程没碰一句 LLM 调用。
  • 如果答案是 应用逻辑 (需要调用多个 API、做条件判断、处理用户多轮意图、生成结构化报告),选 LangChain 。它的 AgentExecutor Runnable 协议,是目前最成熟的 LLM 应用编排方案。注意:LangChain 不是“必须用 agent”, LLMChain + PromptTemplate 就能搞定 70% 的简单场景,别一上来就上 ReAct

更多推荐