CopilotKit 与 LlamaIndex 多模态 Demo 端到端验收指南:图片 / PDF 附件问答实战
【免费下载链接】CopilotKitThe Frontend Stack for Agents & Generative UI. React, Angular, Mobile, Slack, and more. Makers of the AG-UI Protocol项目地址: https://gitcode.com/GitHub_Trending/co/CopilotKit
本指南以 CopilotKit 仓库中showcase/integrations/llamaindex集成的多模态(Multimodal)Demo 为主线,系统讲解如何在 LlamaIndex Agent 后端上实现"上传图片 / PDF 并针对其内容提问"的能力,涵盖前置条件、测试步骤、前后端完整链路、源码级原理与自动化回归验证。读完本文,你将掌握 CopilotKit 聊天界面如何把图片与 PDF 附件送入视觉模型(gpt-4o)并返回基于附件内容的回答,以及如何用 Playwright 端到端用例固化该行为。
多模态 Demo 验收目标
showcase/integrations/llamaindex/qa/multimodal.md是该项目 QA(Quality Assurance)体系中针对多模态演示页的验收文档。它的核心目标非常聚焦:验证 Agent 能否真正"看见"用户上传的图片、读懂用户上传的 PDF,并基于附件内容回答问题,而不仅是把附件当作装饰性的聊天元素。这对应了 CopilotKit 生态中"Bring Your Own Agent、Any Channel"理念下的一个具体落地场景——把 CopilotKit 的对话 UI 与 LlamaIndex 的 Agent 运行时对接,让多模态输入在 AG-UI 协议与 LlamaIndex 工作流之间无损流转。
该功能的完整工程实现位于 showcase/integrations/llamaindex/src/agents/multimodal_agent.py,前端页面、独立运行时路由、示例文件与端到端测试共同构成一个可复现的参考实现。
前置条件
根据 QA 文档,运行与验收该 Demo 需要满足两个硬性前提:
- Demo 已部署且可访问:多模态演示页(
/demos/multimodal)需要处于可访问状态。它的注册信息位于 manifest.yaml,其中声明了 Demo 名称(Multimodal Attachments)、演示路由、依赖的 Agent 源码与前端组件,以及"需要 vision-capable agent (gpt-4o)"这一关键约束。 - Agent 后端配置了视觉能力模型(gpt-4o):这是整个多模态能力成立的基础。
gpt-4o同时接受文本与图像输入,是 Demo 中默认指定的视觉模型;gpt-4o本身并不接受原生 PDF 输入,因此 PDF 需要在 Python 侧先做"压平(flatten)为文本"的预处理(详见下文)。
视觉模型在 Agent 侧的落地方式
在 multimodal_agent.py 中,Agent 通过llama-index-protocols-ag-ui包的get_ag_ui_workflow_router构造:
multimodal_router = get_ag_ui_workflow_router( llm=OpenAI(model="gpt-4o", temperature=0.2, **_openai_kwargs), frontend_tools=[], backend_tools=[], system_prompt=SYSTEM_PROMPT, initial_state={}, )关键参数含义:
llm=OpenAI(model="gpt-4o", temperature=0.2):视觉模型配置。temperature=0.2让回答偏保守、稳定,适合"描述附件内容"这类事实性任务。OPENAI_BASE_URL环境变量:若设置了该变量,会把其值写入api_base透传给 OpenAI 客户端(_openai_kwargs逻辑),方便接入代理或兼容端点。frontend_tools=[]/backend_tools=[]:多模态 Demo 刻意不注册任何工具,把全部上下文预算留给附件内容理解。system_prompt:模块顶部定义的SYSTEM_PROMPT明确指导模型行为——"用户可能附带图片或文档(PDF),当出现附件时仔细分析附件并回答用户问题;若没有附件则正常回答文本问题;回复保持简洁(1-3 句话)除非用户要求深入"。这段提示词是 QA 验收"Agent 基于图片内容回答"的行为来源。
文档头注释还说明了协议层的关键事实:llama-index-protocols-ag-ui==0.4.1会把 AG-UI 协议中的文本、图像与文档内容部件(content parts)转换为对应的 LlamaIndex blocks,交给 workflow router 处理。这也是该 Demo 在requirements.txt中锁定llama-index-core==0.14.24、llama-index-llms-openai==0.7.10、llama-index-protocols-ag-ui==0.4.1三个版本的原因。
PDF 的处理策略:压平为文本
来自前端页面的架构说明(page.tsx):
- 图片以原生形式转发给模型(gpt-4o 原生支持图像输入);
- PDF 在 Python 侧通过
pypdf压平为文本,以实现"provider-agnostic"(不依赖具体模型是否支持 PDF)的统一行为。
这意味着对 PDF 的提问实际上走的是"PDF → 抽取文本 → 放入模型上下文"的路径,对任何文本能力模型都成立,不必强求模型原生支持文档输入。
测试步骤(QA 文档原文验收清单)
QA 文档multimodal.md规定的验收步骤为:
- 进入多模态 Demo 页面(Navigate to the multimodal demo page)
- 上传一张图片并就其提问(Upload an image and ask a question about it)
- 验证 Agent 基于图片内容回答(Verify the agent answers based on the image content)
- 上传一个 PDF 并就其内容提问(Upload a PDF and ask a question about its contents)
下面分别说明每一步在实现层面的对应链路与可验证依据。
步骤 1:进入 Demo 页面
页面入口为/demos/multimodal。前端入口 page.tsx 将页面结构组织为:
<CopilotKit runtimeUrl="/api/copilotkit-multimodal" agent="multimodal-demo"> <LegacyConverterShim /> <MultimodalChat /> </CopilotKit>runtimeUrl="/api/copilotkit-multimodal":指向专属的运行时路由(而非主路由),把视觉模型的调用成本隔离到该 Demo 单独承担,其他演示单元继续使用更便宜的纯文本模型。agent="multimodal-demo":绑定专用 Agent slug。LegacyConverterShim:负责新旧 AG-UI 内容部件形状的兼容重写与附件去重(详见下文"源码原理")。
页面主体MultimodalChat(multimodal-chat.tsx)渲染了一个带示例按钮行和CopilotChat组件的全屏对话区,并注入了一段"一次性弹跳动画"CSS 来吸引用户注意回形针(附件)按钮。
步骤 2 与 3:上传图片并验证基于图片的回答
上传能力的开关在CopilotChat的attachments配置上:
const MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024; // 10 MB const ACCEPT_MIME = "image/*,application/pdf"; <CopilotChat agentId="multimodal-demo" attachments={{ enabled: true, accept: ACCEPT_MIME, maxSize: MAX_FILE_SIZE_BYTES, onUpload, onUploadFailed: (err) => { console.warn("[multimodal-demo] attachment rejected", err); }, }} />参数说明:
enabled: true:开启附件上传能力,聊天输入框出现回形针附件菜单。accept: "image/*,application/pdf":文件选择器只接受图片与 PDF,从入口处约束上传类型。maxSize: 10 MB:单文件上限 10 MB;超出时由CopilotChat的默认 UI 弹出校验失败提示,同时触发onUploadFailed回调记录告警。
onUpload绑定到 file-to-data-attachment.ts 中的fileToDataAttachment:用浏览器FileReader把文件读成data:<mime>;base64,<payload>形式,再剥离前缀,只把原始 base64 作为AttachmentUploadResult的data变体返回(含mimeType与metadata.filename/size)。该 Demo 刻意选择"base64 内联"而非上传到外部存储,保证 Demo 自包含、无外部依赖。
图片上传后,Agent 侧把图片以原生视觉输入交给 gpt-4o,回答即来源于图片内容本身。这也是 QA 步骤 3 的验证点。
步骤 4:上传 PDF 并验证基于内容的回答
PDF 走同一条附件管线,区别在于后端处理:pypdf在 Python 侧把 PDF 内容压平为文本后进入模型上下文。因此对 PDF 的提问同样能得到基于文档内容的回答,且不依赖模型是否原生支持 PDF 输入。
源码原理:一条消息的完整往返链路
专属运行时路由:成本隔离
api/copilotkit-multimodal/route.ts 为该 Demo 单独创建了一个CopilotRuntime:
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000"; const multimodalAgent = new HttpAgent({ url: `${AGENT_URL}/multimodal/run` });AGENT_URL默认指向http://localhost:8000(本地 Agent 服务),可通过环境变量覆盖。- 使用
@ag-ui/client的HttpAgent连接 LlamaIndex 服务的/multimodal/run端点,并把 slugmultimodal-demo与default都映射到该 Agent。 - 路由文件头注释再次强调:gpt-4o 是视觉能力模型,把它隔离到独立 runtime,让视觉模型的成本只由这一个 Demo 承担。
前端示例按钮:与回形针共用的单一代码路径
page.tsx 的架构说明指出:示例文件位于public/demo-files/(即sample.png与sample.pdf),示例按钮组件在客户端 fetch 文件、包装成File,并通过 DataTransfer 驱动与回形针相同的隐藏<input type="file">触发change事件——示例路径与真实上传路径共用同一套onUpload/useAttachments管线,"一个能用一个就能用"。
file-to-data-attachment.ts 中fileToDataAttachment的注释特别说明:maxSize: 10 MB是为了给"浏览器内联 base64"方案兜底,防止把消息体撑爆。
旧版转换垫片:新协议形状与旧转换器的桥接
legacy-converter-shim.tsx 修复了三个真实存在的兼容问题,值得作为多模态接入的参考:
- 出站现代部件对 Agent 不可见:已发布的
@ag-ui/langgraph转换器(0.0.x)只认旧的{ type: "binary", mimeType, data | url }形状,会静默丢弃现代的{ type: "image" | "document", source: {...} }部件。垫片在onRunInitialized中为每个现代媒体部件追加(而非替换)一个binary镜像——不能替换是因为聊天 UI 的getMediaParts只渲染image|audio|video|document,替换会让附件在消息回环后从界面上消失。 - 回环媒体被加倍且类型错乱:转换器把出站的
binary部件发成 LangChain 的image_url,又把传入的image_url一律收回为 AG-UIimage部件,导致 PDF 被标记成type: "image"却带mimeType: "application/pdf",被渲染成加载失败的<img>。垫片在onMessagesSnapshotEvent与onRunFinalized中按source.value去重,并按 mimeType 重新推导类型(image/*→image、audio/*→audio、video/*→video、其余→document),让 PDF 正确落到DocumentAttachment(图标 + 文件名)。 - PDF 压平文本泄漏进聊天气泡:早期实现中 Python 中间件通过
before_model改写状态,导致[Attached document]\n<pdf body>这样的占位文本残留在用户消息里。当前实现改为在模型请求层面作用域内重写(wrap_model_call),彻底消除 UI 泄漏(该点由 E2E 用例固化)。
E2E 测试:把行为固化为回归防线
tests/e2e/multimodal.spec.ts 是multimodal.mdQA 步骤的可执行化版本,用 Playwright 覆盖五个场景:
- 页面加载完整性(示例按钮行、示例按钮、聊天输入框、回形针菜单均可见);
- 示例图片:点击后自动发送预设提示词、用户消息中恰好一个
<img>、无 "Failed to load image"、助手回答引用附件(断言/copilotkit|logo|image/i); - 示例 PDF:用户消息中恰好一个PDF 文档 chip(而非破碎图片)、消息体不含
[Attached document]泄漏文本、助手回答引用附件; - 同会话图片→PDF与PDF→图片:每个用户消息各自保留唯一的正确 chip,无交叉污染、无加倍。
测试文件头还记录了固化的回归点(每个都曾是真实 bug):自动发送被上传竞态吞掉、附件重复显示两个缩略图、PDF 被错误渲染成破图、压平文本泄漏、以及同一会话内连续上传不同类型的交叉污染。测试约定两个预置提示词与showcase/aimock/feature-parity.json中的 canned prompts 一一对应("can you tell me what is in this demo image I just attached" / "…demo pdf I just attached")。
常见故障排查(来自源码注释的实测经验)
多模态 Demo 的源码注释里沉淀了几个实操中易踩的坑:
- Git LFS 指针文件冒充真资源:如果部署环境未执行
git lfs pull,Next.js 可能以Content-Type: image/png返回 Git LFSpointer(以version https://git-lfs...开头的纯文本短桩)。示例按钮组件用魔数校验兜底:PNG 必须以89 50 4E 47开头、PDF 必须以25 50 44 46开头,并在头部 64 字节内检测 LFS pointer 前缀,失败时抛出可操作的错误提示而非把坏字节发给模型。 - 附件显示成两个 chip / 一个破图:多半是
@ag-ui/langgraph回环导致的重复与类型错乱,确认LegacyConverterShim已正确挂载并在onRunInitialized/onMessagesSnapshotEvent/onRunFinalized三个时机执行重写与去重。 - "Cannot send while attachments are uploading":自动发送时若附件仍处于
status: "uploading",CopilotChat.onSubmitInput会拒绝发送并清空输入。示例按钮组件改用 V2 Agent 编程式接口agent.addMessage(...)+copilotkit.runAgent({ agent }),在调用前就把附件组装成已含 base64 的内容部件,从根本上消除上传竞态。
结论与延伸阅读
多模态 QA 验收(进入页面 → 传图提问 → 验证图片回答 → 传 PDF 提问)在本仓库中不是孤立的检查清单,而是由完整工程实现支撑的闭环:gpt-4o视觉模型 + LlamaIndex AG-UI workflow router(multimodal_agent.py)、独立运行时路由(route.ts)、附件配置与 base64 转换(multimodal-chat.tsx、file-to-data-attachment.ts)、协议兼容垫片(legacy-converter-shim.tsx)以及 Playwright 回归用例(multimodal.spec.ts)。参照这些文件,你可以把"上传图片/PDF 并基于内容提问"的能力复用到自己的 LlamaIndex + CopilotKit 集成中。
【免费下载链接】CopilotKitThe Frontend Stack for Agents & Generative UI. React, Angular, Mobile, Slack, and more. Makers of the AG-UI Protocol项目地址: https://gitcode.com/GitHub_Trending/co/CopilotKit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考