Gradio Agents MCP Hackathon 2025 — Agent Track 荣誉奖_Honorable Mention--仅需425 行 Python
引言【内部深度思考 Thinking Process】 ■ 输入/输出梳理 Input: 用户自然语言 query客服问题 多轮对话历史 Output: LLM 生成的、有知识库 grounding 的回答 可观测的检索溯源面板 ■ 核心难点 难点1: 多轮对话中的指代消解coreference 它多少钱 → 它是什么 → 解法: CondensePlusContextChatEngine 的 Condense 步骤用 LLM 把历史新问题压缩成一个 独立完整的问题再送去检索。 难点2: 多知识库路由决策 不能把所有 index 全查一遍延迟高、噪声多 → 解法: LlamaCloudCompositeRetriever(modeROUTING) —— 给每个 index 挂一段 自然语言 description内部 LLM Router 根据 query 语义匹配 description 来决策 去哪个抽屉找材料而不是把所有抽屉翻一遍。 难点3: Gradio 响应式 UI避免长时间白屏等待 → 解法: 两阶段链式事件.then() 链第一阶段 queueFalse 立即更新 UI 显示 Retrieving...第二阶段 queueTrue 排队等 LLM 结果返回再刷新。 难点4: 幻觉控制 元数据感知 → 解法: temperature0 system_prompt 双重约束 明确要求 LLM 从 context 的 metadata 中提取文件信息。 ■ 资料缺失时的合理推测 LlamaCloud 内部的 ROUTING 实现没有开源但根据 LlamaIndex 官方文档和论文 其内部很可能是一个 function-calling 或 tool-selection 形式的 LLM 调用 把各 index 的 description 作为 tool description 传入让 LLM 决定调用哪个工具。 rerank 步骤很可能使用 Cohere Rerankrequirements 隐含GitHub topic 标签有 cohere-embeddings。项目全局视角 (Overview)一句话定位这是一个用 LlamaIndex 托管检索服务 Claude Sonnet Gradio 搭建的多知识库智能路由客服机器人其核心价值在于用极简的代码425行把企业多部门文档 → 精准路由 → 对话式问答这条工程链跑通并内置可观测性。业务痛点传统客服系统面临的经典三角困境痛点表现该项目的解法知识碎片化产品手册、FAQ、账单政策分散在不同部门4个独立 LlamaCloud Index各司其职意图识别不准全文搜索关键词匹配跨库检索噪声大ROUTING 模式LLM 做意图分类再定向检索多轮对话失忆每轮当作新问题无法理解它/这个政策CondensePlusContext用 LLM 把历史压缩成独立问题黑盒不可信客服回答没有来源无法溯源审计右侧检索溯源面板暴露 Index名/文件名/置信分胜负手这个项目最聪明的设计不是 LLM 本身而是**把****“意图路由”**这个决策外包给了 LlamaCloud 的托管 LLM Router并用自然语言 description 作为路由信号——无需训练任何分类器无需维护路由规则表只需写好几句话描述每个知识库是干什么的。多模态数据摄入与解析 (Data Ingestion Parsing)What文档类型组合ProductManuals/ → PDF产品手册含技术规格、操作步骤 FAQGeneralInfo/ → PDF常见问题、公司政策 BillingPolicy/ → PDF × 4计费政策含 v1/v2 版本迭代 CompanyIntroductionSlides/ → PPTX公司介绍幻灯片 CSV 元数据注意两个非常规点PPTX 文件LlamaCloud 会逐 slide 解析page_label字段对应幻灯片页码代码第205-208行有专门处理CSV 元数据文件每个子目录都有一个*_metadata.csv这是手工补充的文档元数据作者、创建日期、修改日期等弥补 PDF 原生 metadata 字段经常为空的缺陷# app.py L202-208: 元数据感知的页码提取file_namemetadata.get(file_name,N/A)iffile_name.lower().endswith(.pptx):page_labelmetadata.get(page_label)ifpage_label:page_infof p.{page_label}Why为什么用 LlamaCloud 托管解析而不是自己跑 OCR方案优点缺点自建 Docling/PyMuPDF完全可控可以自定义 chunk 策略需要维护解析管线、处理公式/图表/表格的 corner caseLlamaCloud 托管零代码接入自动处理 PDF/PPTX/CSV内置 pipeline 解析黑盒依赖第三方服务成本随量计费作者的选择是 LlamaCloud 托管这是一个典型的换钱买时间的工程决策在Hackathon 场景下完全合理。HowChunking 策略推测LlamaCloud 的默认 chunking 对 PDF 是语义感知切块非固定字符数截断PDF → 按段落/标题层级切块PPTX → 按 slide 切块每张幻灯片是一个 nodeCSV 元数据 → 逐行映射到对应文档的 metadata 字段而非作为独立可检索节点这解释了为什么 system prompt 里特别说“When asked about file-specific details like theauthor, creation date, or last modification date, retrieve this information from the document’smetadataif available in the provided context.”元数据是附着在 node 上作为 metadata dict 传入 LLM context 的不是通过语义检索得到的。版本控制的隐式实现BillingPolicy 文件夹里同时有late_payment_policy.pdf和late_payment_policy_v2.pdf。用户问最新的逾期政策是什么时语义检索可以同时召回两个文件而 LLM 可以根据文件名中的v2判断哪个更新——这是一种不需要任何额外代码的版本感知纯靠文件命名规范 LLM 语义理解实现。检索与召回引擎 (Retrieval Reranking)这是整个项目最核心的部分值得逐行拆解。LlamaCloudCompositeRetriever架构核心# app.py L87-93composite_retrieverLlamaCloudCompositeRetriever(nameCustomer Support Retriever,project_nameLLAMA_CLOUD_PROJECT_NAME,create_if_not_existsTrue,modeCompositeRetrievalMode.ROUTING,# ← 关键参数rerank_top_n2,# ← 严格截断到2个节点)ROUTING vs FULL 模式对比参数FULL 模式ROUTING 模式查询哪些 Index全部LLM 决定的子集通常1个延迟高并发查N个Index低只查1-2个召回精度可能混入不相关Index的结果定向精准适用场景跨域综合查询意图明确的客服场景ROUTING 模式的本质是把多数据源检索问题转化为**“先做一次意图分类再做单数据源检索”**的两步问题。Description-as-Routing-Signal 模式# app.py L98-113composite_retriever.add_index(product_manuals_index,descriptionInformation sourcefordetailed product features,technical specifications,troubleshooting steps,andusage guidesforvarious products.,)composite_retriever.add_index(billing_policy_index,descriptionProvides information related to pricing,subscriptions,invoices,payment methods,andrefund policies.,)# ... 其余两个 index 类似这段代码的精妙之处LlamaCloud 内部的 Router推测实现如下# 伪代码还原 LlamaCloud ROUTING 模式的内部逻辑tools[{name:ProductManuals,description:... technical specs, troubleshooting ...},{name:FAQGeneralInfo,description:... common questions, policies ...},{name:BillingPolicy,description:... pricing, invoices, refunds ...},{name:CompanyIntroSlides,description:... company overview, leadership ...},]# 用 LLM function calling 选择要查哪个 toolindexselected_indexllm.select_tool(querycondensed_question,toolstools)# 只查选中的 indexresultsselected_index.retrieve(condensed_question)关键洞察description 字段就是路由器的地图标签。写得越清晰、边界越分明路由准确率就越高。作者在描述中特意选用了**领域关键词**troubleshooting、invoices、refund policies、leadership这些词与用户可能使用的自然语言高度重叠降低了语义 gap。Reranking 策略rerank_top_n2# 从查询结果中只保留 top 2 个节点Why 只要 top 2这是一个非常激进的截断策略背后的工程考量是减少 LLM context 长度→ 降低 token 成本减少答案被淹没的风险强迫 reranker 做精准筛选→ 把召回质量的压力前置到 reranker而不是让 LLM 在大量噪声中自己找客服场景的特点→问题通常有唯一正确答案top 2 的精确率足够覆盖大多数问题Reranker 的实现由 LlamaCloud 托管根据 GitHub topic 标签cohere-embeddings推断底层很可能使用Cohere Rerank APIcross-encoder 架构比双塔向量检索精度高得多。检索透明度面板# app.py L188-216解析 source_nodes展示给用户fori,nodeinenumerate(nodes):node_blockf [Node{i1}] Index:{metadata.get(retriever_pipeline_name,N/A)}# ← 是哪个知识库 File:{file_name}{page_info}# ← 具体文件页码 Score:{score}# ← rerank 置信分 这个右侧溯源面板是该项目最有工程价值的 UX 设计在不需要用户懂 RAG 的情况下让 QA 工程师可以实时监控**“路由是否正确、文件是否命中、分数是否合理”**。Agent 编排与防幻觉 (Agent Workflow Anti-Hallucination)CondensePlusContextChatEngine完整执行链路# app.py L117-133memoryChatMemoryBuffer.from_defaults(token_limit4096)chat_engineCondensePlusContextChatEngine.from_defaults(retrievercomposite_retriever,memorymemory,system_prompt...,verboseTrue,)完整的每轮对话执行链如下Condense 步骤的核心价值没有 Condense 时的问题经典 RAG 的失败模式Turn 1: What is the late payment policy? Turn 2: Does it apply to annual subscriptions? ↑ 如果直接用这句话去检索向量数据库根本找不到相关内容 因为it没有明确指代apply也过于泛化有 Condense 后Turn 1: What is the late payment policy? → 检索 → 回答 → 存入 Memory Turn 2: 原始问题 Does it apply to annual subscriptions? Memory 中的 Turn 1 ↓ Condenser LLM 处理 → Does the late payment policy apply to annual subscriptions? ↓ 用这个完整问题去检索 → 精准命中 BillingPolicy Indexraw_input[message]** 的提取技巧**# app.py L169-176从 AgentChatResponse 中挖出压缩后的问题ifhasattr(response,sources)andresponse.sourcesisnotNone:context_sourceresponse.sources[0]if(hasattr(context_source,raw_input)andcontext_source.raw_inputisnotNoneandmessageincontext_source.raw_input):condensed_questioncontext_source.raw_input[message]这是一段不在文档里、需要读源码才能发现的逆向工程技巧LlamaIndex 的AgentChatResponse.sources[0].raw_input[message]隐藏了发给 Retriever 的实际 query即压缩后的问题作者把它暴露到 UI 上供调试。防幻觉的多层防御# 第一层LLM 温度为 0Settings.llmAnthropic(modelclaude-sonnet-4-0,temperature0)# → 消除随机性迫使 LLM 输出最高概率最保守/准确的 token 序列# 第二层System Prompt 的权威陈述指令 Provide accurate answers from product manuals, FAQs, and billing policies... Never refer to or mention your information sources (e.g., the manual says, from the document). State facts authoritatively. # → 防止 LLM 使用the document says...这类可能引发幻觉的引用模式# 强迫它用事实陈述语气减少不确定性措辞# 第三层Token 限制的 MemorymemoryChatMemoryBuffer.from_defaults(token_limit4096)# → 超出 4096 token 时自动截断早期对话# 防止过长历史引入噪声同时控制成本# 第四层Retrieval Grounding本质上是 RAG 的核心价值# → LLM 只能基于 retrieved context 回答无法凭空创造Gradio 两阶段异步模式# app.py L362-385两阶段链式事件submit_eventmsg.submit(fninitial_submit,# 阶段1: 立即更新UI无需等LLMinputs[msg,chatbot],outputs[chatbot,msg,retriever_output,user_message_state],queueFalse,# ← 不排队立即执行).then(fnget_agent_response_and_retriever_info,# 阶段2: 调用LLMinputs[user_message_state,chatbot],outputs[chatbot,retriever_output],queueTrue,# ← 可排队处理并发请求)为什么需要user_message_state# 阶段1 执行 initial_submit 时msg textbox 被清空了返回 # 阶段2 需要用原始 message 调用 LLM# 如果直接用 msg 作为输入得到的是空字符串# 解决方案用 gr.State 在阶段1和阶段2之间传递原始 messageuser_message_stategr.State(value)这是 Gradio 异步编程中的一个经典陷阱作者用**gr.State**完美规避。Feynman 大白话类比类比一ROUTING 模式 智能前台接待员想象一个大公司的前台接待。公司有4个部门产品部处理产品问题、客服部FAQ、财务部账单、公关部公司介绍。没有 ROUTING 模式FULL 模式前台把你的问题同时转给所有4个部门每个部门都给你回复一封邮件最后秘书从所有邮件里挑出最相关的2条给你读。效率低还容易引入不相关部门的乱答。有 ROUTING 模式前台是个聪明人听你说我这个月的账单有问题直接说你去找财务部只派一个部门处理速度快、答案准。而聪明的来源就是每个部门门口挂的那块牌子上的描述description 字段——前台看牌子就知道该敲哪扇门。类比二CondensePlusContext 速记员 图书馆员的黄金搭档假设你在图书馆跟一个图书馆员RAG 系统聊天你说第1轮“我想了解逾期还款政策。”图书馆员去书架找来了相关章节给你读了一段。你说第2轮“那它对年度订阅用户也适用吗”问题来了你说的它和那图书馆员根本不知道指什么如果直接拿那它对年度订阅用户也适用吗去搜索书架什么都找不到。CondensePlusContext 的做法在图书馆员去书架之前旁边有个速记员Condenser LLM他翻看你们之前的对话记录把你说的话翻译成“逾期还款政策对年度订阅用户是否适用”——一个完整的、不含指代的独立问题。然后图书馆员拿着这个完整问题去书架一击即中。三个可以直接抄作业的工程 TrickTrick 1Description-as-Intent-Signal 路由模式适用场景任何多知识库、多数据源的企业 RAG 系统。# 工程精髓description 是路由的灵魂写好它比调参数更重要# 三条黄金原则# 1. 用领域专属词domain-specific terms避免通用词# 2. 明确边界exclude 比 include 更重要# 3. 覆盖用户可能用的口语化表达composite_retriever.add_index(billing_index,description(Provides information related to pricing, subscriptions, invoices, payment methods, and refund policies. # ↓ 加上反例帮助 router 区分边界可选但效果好# Does NOT cover product usage or company background.),)# 可复用 Pattern多租户 SaaS 的知识库隔离# 每个客户/部门 一个 LlamaCloudIndex# 通过 description 精准路由无需训练分类器Trick 2Gradio 两阶段链式事件 gr.State 传递原始输入# 适用场景任何需要立即反馈 后台异步处理的 Gradio 应用# 例如图像生成、文档摘要、代码分析等user_input_stategr.State(value)# 跨阶段传递原始输入defstage_1_instant_feedback(user_input,history):阶段1立即更新UI告知用户正在处理history.append({role:user,content:user_input})returnhistory,,⏳ Processing...,user_input# 清空输入框保存原始输入到 Statedefstage_2_heavy_processing(original_input,history):阶段2执行耗时操作resultcall_llm_or_any_heavy_api(original_input)history.append({role:assistant,content:result})returnhistory,✅ Done: result# 链式绑定submit_btn.click(fnstage_1_instant_feedback,inputs[user_input,chatbot],outputs[chatbot,user_input,status_box,user_input_state],queueFalse,# 立即执行不排队).then(fnstage_2_heavy_processing,inputs[user_input_state,chatbot],outputs[chatbot,status_box],queueTrue,# 排队执行支持并发)Trick 3从 AgentChatResponse 挖掘 Condensed Question逆向工程# 适用场景任何使用 CondensePlusContextChatEngine 的项目# 用于调试、日志记录、用户体验优化defextract_debug_info(response:AgentChatResponse)-dict: 从 AgentChatResponse 中提取关键调试信息 这些信息 LlamaIndex 文档几乎没有提及靠读源码发现 debug_info{condensed_question:N/A,source_nodes:[],}# 1. 提取压缩后的独立问题实际发给 Retriever 的 queryifhasattr(response,sources)andresponse.sources:context_sourceresponse.sources[0]if(hasattr(context_source,raw_input)andcontext_source.raw_inputandmessageincontext_source.raw_input):debug_info[condensed_question]context_source.raw_input[message]# 2. 提取每个 retrieved node 的溯源信息fornodein(response.source_nodesor[]):metanode.metadataor{}debug_info[source_nodes].append({index_name:meta.get(retriever_pipeline_name),# 哪个 Indexfile_name:meta.get(file_name),# 哪个文件page:meta.get(page_label),# 哪页score:node.score,# rerank 分数})returndebug_info# 用途# 1. 在生产环境记录日志监控路由准确率# 2. 在开发阶段调试快速发现为什么路由到了错误的 Index# 3. 作为 UI 透明度面板的数据源本项目的做法完整架构 Mermaid 图关键局限性与改进方向现有局限根本原因进阶改法rerank_top_n2过于激进复杂多文档问题可能漏答追求精度牺牲了召回率动态 top_n简单问题2复杂问题5用 query 复杂度分类器判断ROUTING 模式无法处理跨 Index 的综合型问题每次只路由到1个 Index引入 ROUTINGFULL 混合模式或先路由、再 agent 决定是否追加查询没有流式输出Streamingchat_engine.chat()是同步阻塞调用改用chat_engine.stream_chat() Gradio Generator 模式无法感知查不到答案并优雅降级没有空结果处理逻辑检测source_nodes为空时返回我暂时找不到相关信息请联系人工客服Memory 截断策略粗暴直接裁掉早期对话ChatMemoryBuffer的默认行为换用SummaryMemoryBuffer对早期对话做摘要而非丢弃最后一句硬核总结这个项目的精髓不是任何单一技术而是证明了一件事——在正确的抽象层次上组合现有工具远比自己重造轮子更高效。LlamaCloud 托管了解析和检索Claude 处理语言Gradio 处理 UIArize 处理可观测性作者只需用 425 行 Python 把它们粘合在一起就实现了一个生产级别可用的智能客服系统。这本身就是一种工程智慧。参考项目Agentic RAG chatbot that intelligently routes customer queries to relevant knowledge bases for accurate responses.https://github.com/karenwky/cs-agent