⌕ ESC
↑↓ 选择↵ 打开esc 关闭⌘K 唤起
← 主页 NO.225
专栏 · Agent 工程手艺 ›
第 225 期 · AGENT 工程 · 收录于 2026 年 9 月 19 日

MCP Apps:数据给模型,界面给用户——Dustin Mihalik(Indeed)

DM
Dustin Mihalik · AI Engineer
Agent 工程AI 产品
视频 15:35 原文约 1.3 万字 预计阅读 14 分钟 来源视频 ↗ 中英对照全文
TL;DR · 三句话
  1. 一界面自己调 API 取数,对模型就是黑箱,用户一追问「讲讲第一条」「给公司排个序」它就答不上。所以规则一是「凡是展示给用户看的东西,也都要作为数据提供给模型」:structuredContent 和界面一起返回、保持同步,用户在界面里点了什么也用 updateModelContext 推回去;规则二是在 tool description 开头写一句「结果已作为 UI 组件自动展示给用户」,免得模型再用文字把结果重排一遍。→ 详细
  2. 二最反直觉的一课:界面做好了,产品反而变差。纯文本 MCP 时,Claude 会做 10 次、15 次搜索,过滤后拼一张表;一旦告诉它「结果会展示在 UI 里」,它调一次就会想「哦,结果应该已经展示出来了,那我就不深挖了」。→ 详细
  3. 三解法是优先级压过前两条的规则三「把数据处理和 UI 渲染分开」:search_jobs 只回数据、想调几次调几次,render_jobs_widget 只收筛好的 ID,还可以顺带收下模型「为什么选它」的理由。收尾两条要点:先关注数据再关注界面,渲染只是模型探索完数据之后的副产品;工具要小而可组合。→ 详细
01

开场:Indeed 平台组来讲「一线教训」——给 Claude、ChatGPT 和自家求职 agent Career Scout 做 MCP Apps

  • Dustin Mihalik 在 Indeed(他介绍为「全球第一的求职网站」)做职位搜索,同时在 AI 平台团队管 guardrails(护栏:拦住模型越界输出的规则)、网关(gateway:所有模型调用统一经过的出入口)和合规;因为和各家模型厂商有合作关系,团队偶尔能接到「很酷的项目」,MCP Apps 和 MCP connector 就是其中之一。→ 详细
  • MCP(Model Context Protocol,让 Claude、ChatGPT 这类模型调用外部工具和数据的标准接口);MCP Apps 是它的界面扩展:工具除了回数据,还能在聊天窗里嵌一块可交互的网页界面。前面的讲者已经把入门讲过了(机制入门可对照 ep119),他这场讲「来自一线的经验教训」:给 Claude、ChatGPT,以及 Indeed 给求职者用的 agent「Career Scout」做 MCP 和 MCP Apps,都学到了什么。例子几乎全是职位搜索的截图。→ 详细
02

纯文本回复能用,但没品牌、难带链接——光让 Claude 稳定附上链接,就烧掉「多得离谱」的工时和 eval

  • 起点是一次纯文本的 MCP 搜索:「我想在奥斯汀找咖啡师的工作」。回复「效果其实挺好」,Claude 自己把结果分成奥斯汀市区和周边郊区的职位。问题有两处:界面上没有任何 Indeed 品牌,公司能掌控的东西有限;更头疼的是链接。→ 详细
  • 他说「除非你亲自试过,否则你真的会很意外」:想让 Claude 或 ChatGPT 附上链接非常难,因为它们不希望你离开它们的环境。他觉得这完全可以理解,但回复要是只剩「这里有五个职位,就在互联网上的某个地方」、一个链接都没有,体验就太糟了。为了让 Claude 稳定地给出链接,他们花了「多得离谱的工时」,跑了无数轮 eval(评测:批量跑测试用例、给模型输出打分)。→ 详细
03

MCP Apps 把掌控权拿回来:申请按钮、重点前置、详情弹窗不离开对话——「双赢」

Indeed 在聊天窗里渲染的 MCP App 职位卡片 ▲ 图注:用户说「I'm looking for a Barista job in Austin」,Indeed 的 MCP App 在对话里排出一排可横向滑动的职位卡片(如 Carpenter Hotel、Marriott International 的咖啡师岗),每张列出薪资、工作类型、办公形式和摘要,底部是 Apply on Indeed 按钮和 View details 链接。

  • 有了 MCP Apps 和 Apps SDK(OpenAI 给 ChatGPT 做应用的开发包),这些都能自己定:放一个「申请」按钮,决定哪些信息重要、要突出在最上面,再给一个「查看详情」链接,点开弹出 modal(弹窗)显示完整职位详情,用户不用离开当前环境。他称之为对两家公司都好的「双赢」。→ 详细
  • 随即提醒:想做 MCP App,或者想「把我的网站搬进 ChatGPT 或 Claude」,「可不是往聊天界面里拖拽一下那么简单」,真正要想清楚的是数据怎么表示、怎么提供给用户。后面三条规则都从这里展开。→ 详细
04

天真做法:界面自己调现有 API 取数,对模型就是个黑箱——用户一追问,它全答不上

UI 对模型是黑箱:三个追问得到三个茫然回答 ▲ 图注:黑箱的后果:用户追问「Tell me about the first result」「Which of these are in-person?」「Please rank the companies」,模型只能回「???」「Huh?」「IDK」,因为界面上显示了什么,它一无所知。

  • 他说的「很天真的做法」:加载数据时还是调你现有的 API(网页前端原来那套取数接口),界面自己拿数据、自己画。整条链路于是变成:用户说「我想做某件事」,模型说「好,我来调一个 tool」,tool 说「好,我来展示点东西」,但「模型完全不知道你展示的是什么」。→ 详细
  • 代价全落在追问上:「给我讲讲第一条结果」「请给这份公司列表排个序」,这些再自然不过的下一句,模型都接不住,因为它根本不知道界面上显示的是什么数据。聊天产品的长处本来就是能接着问;数据只进了界面、没进模型,等于把这个长处关掉了。→ 详细
05

规则一:凡是给用户看的,也都要作为数据给模型——structuredContent 和 resourceUri 一起返回,还要保持同步

代码:resourceUri 通往界面,structuredContent 是给模型的数据 ▲ 图注:一个 get-time 示例工具的注册代码:箭头标出 _meta.ui.resourceUri 是「Path to UI」(把工具连到它的界面资源),返回值里的 structuredContent 是「Data as structuredContent」,也就是给模型的那份数据。

  • 这是他做 MCP Apps 的第一条规则,原话是「凡是展示给用户看的东西,也都要作为数据提供给模型」。他说这听起来理所当然,但确实见过一些 MCP App 只是拿它「往页面里注入一些 HTML,然后调几个 API」,对模型来说「就是一个大黑箱」。→ 详细
  • 规范里做起来很容易,靠两个字段:structuredContent(结构化内容:工具返回给模型的那份数据)「就是你只做纯文本 MCP 时本来就会返回的东西」;resource URI(资源地址)指向界面的 HTML 放在哪。这两样都得返回,而且要保持同步:API 里加了新东西,就一定要把同样的数据也加回给模型那一份。→ 详细
  • 说白了,同一次工具调用,一份数据走两个出口:界面拿去画,模型拿去读,界面上有的,模型那份里不能缺。这一条与同批 ep226 Anthropic 的 presentation tool 思路互为印证。→ 详细
06

规则二:在 tool description 开头告诉模型「结果已作为 UI 组件展示」,否则界面和模型会「打架」

  • 数据给到之后,下一个问题马上冒出来:模型手里有数据,旁边又有一个它一无所知的界面,它「还是会像平时一样,拿着输出再描述一遍」。结果是上面一份界面展示,下面模型又用纯文本把同样的结果排了一遍。→ 详细
  • 解法是改 description(工具说明:模型决定何时、怎么调用这个工具时读的那段文字),明确告诉它结果会在 MCP App 里展示。理想效果是模型只给「一小段总结」,结果和链接已经展示在上方,而不是它「自己再搞一整套纯文本展示」。→ 详细
  • 他把这叫 UI 显示什么和模型输出什么之间「有点打架」,可以用 description 去引导。哪怕只是在 tool description 开头加一句「结果已作为 UI 组件自动展示给用户」(原话 results were automatically displayed to the user as UI components),就「能覆盖相当多的情况」。→ 详细
07

用户在界面里点了什么,模型同样看不见——用 updateModelContext 把交互推回给模型

代码:购物车例子用 updateModelContext 把状态推回模型 ▲ 图注:MCP Apps 文档里的购物车例子:把商品数、总价、币种和购物车里的每件商品拼成一段 markdown 文本,高亮处用 app.updateModelContext 把它推回给模型。

  • 下一个坑在可交互的部分:「申请」按钮、「查看详情」按钮(点开弹出一大段职位描述)。用户点开详情,很可能接着问「帮我给这个职位写封求职信」「总结一下这份职位描述」,模型却完全不知道:数据要么是经 API 加载的,要么是动态加载进来的。比如你返回了 10 个职位,用户点了其中一个,「模型根本不知道你点的是哪一个」。→ 详细
  • 所以「任何关于用户交互的信息,你也都要提供给模型」。规范里的现成办法是 updateModelContext(更新模型上下文:由界面主动往模型的上下文里写一段话),但只能传一个字符串:想随时间追踪多个事件,就得把多条内容往这个字符串里追加。文档示例是一个购物车应用,把总价和购物车里所有商品都写进去,用户就能问「告诉我购物车里这些商品的信息」。→ 详细
08

反直觉的一课:界面做好了,产品反而变差——纯文本时 Claude 会搜 10 到 15 次再拼表,挂上界面后只调一次就收手

  • 做到前两条,你得到的是一个「模型大致能看清发生了什么」的应用,但他说这「还不一定算得上一个真正好的 MCP App」,因为它只是给了你一些看起来符合预期的界面。→ 详细
  • 问题出在人们真正怎么用 Claude。他的原话是「我不会把简单的问题交给 Claude,我交给 Claude 的都是真正难的问题」:只想搜一次的话,直接上网搜就好了。他举的查询(截图是两天前截的,他自嘲上面「还没有 Sonnet 5」)是:我想找某个头衔的职位;我愿意搬家,所以要在一大堆城市里搜;我要薪水最高的,你帮我挑几个;还有些行业我绝对不去。纯文本 MCP 干这种活「表现非常好」:Claude 会做 10 次、15 次不同的搜索,做过滤,把零散信息一条条提取出来,最后在底部给一张漂亮的表格。→ 详细
  • 换成刚才那种 MCP App 就变味了:description 里已经告诉模型「结果会展示在这个 UI 里」,Claude 调用一次就会想「哦,结果应该已经展示出来了,那我就不深挖了」。用户也不想看 10 个不同的 carousel(轮播:可左右滑动的一排卡片),Claude 还会注意到自己已经展示过东西,于是不会再调 10 次。前两条规则治好了模型「看不见」,却把纯文本时那种多轮深挖给弄没了。→ 详细
09

规则三,优先级压过前面所有规则:把数据处理和 UI 渲染分开

  • 他说真正该做的是第三条规则,「它的优先级高于前面所有规则」:把数据处理和 UI 渲染分开(separate your data processing from your UI rendering)。这句措辞他坦白是从 OpenAI 的 Apps SDK 文档里「偷来的」,文档里好几处都这么写。→ 详细
  • 落到做法上:原来的职位搜索退回成一个标准的纯文本 MCP 工具,「Claude 想调多少次就调多少次」;再单独做一个 render tool(渲染工具:只负责把选定的结果画成界面)。给它传数据有两种方式:让模型把要渲染的数据全部传进去,或者只传一个引用。→ 详细
10

Indeed 的实际拆法:search_jobs 只回数据,render_jobs_widget 只收一串 ID——先搜上百个、筛到五个、只画这五个

幻灯片:一个工具拆成 search_jobs 加 render_jobs_widget ▲ 图注:规则三的落地:原来的 search_jobs(title, location) 拆成两个工具,search_jobs(title, location) 只管搜,render_jobs_widget(job_ids) 只管画,描述里注明「Always call search_jobs first」。

  • 改法是:原来只有一个 search jobs,现在是一个「不返回任何 UI 的 search jobs」,外加一个「接收 ID 列表的 render jobs widget」。这样 Claude 可以先搜索,拿到上百个它关心的职位,自己过滤,找出看中的五个,再只把这五个展示给用户。纯文本时代那种多轮搜、筛、比的深度回来了,界面也保住了。→ 详细
  • 多出来的活在 description 上:做 render 调用时,要在说明里写清数据从哪来、应该是什么格式。他说这是个「相当简单的机制」,在 tool description 里写明「你必须总是先调用这三个 tool 之一,拿到要用来渲染的数据」就行。→ 详细
  • search jobs 只是一个例子。他认为大多数行业都能想清楚「我想在哪里拆」:先让模型探索数据,再让它回过头来自己决定渲染什么。比如电商,一大堆可选的商品;再比如地图,可以让模型先找出五个不同的地址,再把这些地址传给渲染工具。→ 详细
11

让模型往界面里「注入个性」:render tool 除了 ID,还可以收「为什么选它」和要高亮的段落

  • 起因是纯文本场景里的一个观察:模型常会主动说「这就是我选这一个的理由」或「这就是它特别好的原因」。所以他们考虑给 render jobs widget 加参数,比如「给我们一个 ID,再附上你认为它合适的理由」,或者「给我们一个 ID,并高亮职位描述里特别好的那一段」。→ 详细
  • 他的结论是 render tool 上「可以发挥很多创意」,让模型往里注入一些额外的「个性」,用户体验会好得多。换个说法:模型的判断本身也可以是渲染参数,卡片上就不只有职位,还带着「为什么推荐给你」。→ 详细
12

收尾两条:先关注数据、再关注界面;工具要小而可组合

收尾幻灯片:两条关键要点 ▲ 图注:收尾幻灯片「Key Takeaways for MCP Apps」只有两行:Focus on the data before the UI(先关注数据,再关注界面),Small, composable tools(小而可组合的工具)。

  • 第一条:先关注数据,再关注 UI。他承认这听起来和原先的想法正好相反:大家本来觉得 MCP Apps「就是我把 UI 放进 ChatGPT 的办法」。他的看法是,想做出真正好的 MCP Apps 体验,得先看清想把哪些数据给模型、想让它拿这些数据做什么;「渲染只是这件事的副产品」,或者说是模型探索完数据之后的结果。→ 详细
  • 第二条:小而可组合的 tool。搜职位如果有两三种不同的方式,就做两三个 search tool,配一个 render tool;也可以配两个 render tool,一个渲染职位列表,一个突出展示某个特定职位。这样就得到「一堆小得多的 tool」。→ 详细
  • 小工具的好处有两头:每个 description 都可以写得相当简单,「不会让模型负担过重」;同时给了模型灵活性,让它自己决定怎么探索数据、怎么渲染。这和规则三一脉相承:工具切得越细,模型能自由组合的余地越大。→ 详细
13

闪电问答 / 收尾

本期为大会演讲,无问答环节。Dustin 比预定时间提前几分钟讲完,说自己没有展位,会在走廊里转悠,欢迎有问题的人去找他。→ 详细

14

联系方式

片中未念出具体账号,只说在大多数社交平台上,他的账号要么是他的姓,要么是名字首字母加姓。→ 详细 视频简介附讲者主页 dmihalik.com。

接着读