
如果你正在写一个 AI 代理第一步大概率是让它去读取某个网页。真正做过的人都会遇到同一个尴尬网页源码密密麻麻HTML 标签里塞着导航、脚本、样式、埋点真正有价值的内容可能只有三百字。传统爬虫会写 CSS 选择器逐条提取但 AI 代理没有耐心去适应每个网站的 DOM 结构。一个更本质的问题是HTML 是为人的视觉设计的不是为机器的语义理解设计的。它缺少标题层级、正文段落和代码块的清晰边界AI 模型在解析这样的内容时不仅要消耗大量 token还容易把广告、推荐位、评论区的噪声一并吞进去。让服务端通过 HTTP Accept 标头返回 Markdown是一种越来越值得推广的 AI 友好做法。客户端在请求中带上Accept: text/markdown服务端若有能力就把正文以干净、结构化、无布局噪声的 Markdown 返回如果没有能力则回退到 HTML。这个机制本身不算新事物HTTP 1.1 很早就定义了内容协商但在 AI 代理成为主要流量来源的当下它值得被重新审视和实现。这篇文章会拆解它的原理、服务端实现、代理端请求方式、验证方法以及你真的上线后可能踩到的坑。读完你至少能回答三个问题为什么说 Markdown 比 HTML 更适合给 AI 代理消费如何用 Node.js 或 Python 快速实现一个支持 Accept 内容协商的接口当代理请求的 URL 是普通网页时怎么让服务端安全地返回 Markdown 而不是 HTML整个过程不依赖特定大模型只需要理解 HTTP 基础所以哪怕你后端经验不多也可以照着落地。1. 为什么 AI 代理需要 Markdown 内容1.1 HTML 对 AI 并不友好HTML 的设计初衷是渲染给浏览器用户看标签本身携带了大量与正文无关的信息。一个典型的博客页面里header、nav、aside、script、style层层嵌套DOM 树可能非常深。AI 代理拿到这样的页面后常见的做法是先调用一个“HTML 转 Markdown”的工具比如trafilatura、readability、playwright解析后的文本提取再把结果拼进提示词。问题在于这类提取工具对每个网页的适应性不同遇到结构奇怪的页面就会出现正文丢失、标题错位、代码块被截断的情况。从 token 消耗的角度看HTML 噪声的代价也很大。假设一篇文章正文 1000 字但 HTML 源码有 30 KB模型只能看到截断后的前几千 token原本应该被重点分析的结论可能根本进不了上下文。更麻烦的是很多网页通过 JavaScript 动态渲染正文直接用 HTTP 请求拿到的 HTML 里根本没有正文AI 代理要么启用无头浏览器要么干脆放弃。也就是说HTML 在语义清晰度、提取稳定性和 token 经济性三个维度上都不是给 AI 消费的好格式。1.2 AI 代理工作流中的真实场景AI 代理常见的任务包括根据用户问题搜索资料并总结、读取指定文档后生成日报、把网页内容存入知识库、监控竞品页面变化、抓取文章批量生成摘要。在这些场景里代理通常需要先“读”网页再“想”如何回答。如果读入的是纯净的 Markdown后面的推理步骤会简单很多。因为 Markdown 已经把标题、列表、链接、代码块、引用都结构化出来了模型可以直接理解文档的骨架。举一个实际场景你给代理下达指令“总结官网最近三个版本的新特性”。代理可能需要请求 changelog 页面、release notes 页面和博客页面。如果这些页面都支持Accept: text/markdown代理拿到的内容是整齐的## v1.8.0、### New Features、- 新增...它不需要再写一套选择器去定位.changelog-content li。在 RAG 知识库场景中Markdown 内容分块也更容易因为标题层级天然就是切分块的好依据比按照 HTML 标签切分更稳定。1.3 Markdown 的独特优势对比几种常见的网页内容表示方式Markdown 处在“结构信息”和“解析成本”的平衡点。格式结构保留视觉噪声模型解析成本下游工具兼容性HTML强但标签噪声高很高高需要清洗浏览器友好AI 不友好Markdown中等标题列表代码块明确低低Typora、VS Code、飞书、SSE 渲染器都支持纯文本弱只剩段落无低但语义弱通用但结构信息丢PDF取决于生成方式中很高需要 OCR需要额外解析库Markdown 最大的特点是人机双读。人可以在 Typora、VS Code 里直接编辑和阅读AI 也可以把它当作接近自然语言的结构化文本直接消费。对于已经存储为 Markdown 的内容服务端要做的只是把它原样返回对于只有 HTML 内容的站点则需要一个转换层。好消息是Markdown 这几年已经成为文档协作、AI 输出和开发者工具的事实标准你为它做的投入不会白费。2. Accept 标头的原理与内容协商2.1 HTTP 内容协商基础HTTP 协议定义了一组机制让客户端和服务端就“返回哪种表示形式”达成一致。最常见的表示维度包括自然语言Accept-Language、字符集Accept-Charset、编码Accept-Encoding和媒体类型Accept。Accept标头的值是一系列 MIME 类型例如text/html、application/json、text/markdown。需要区分两个容易混淆的标头Accept和Content-Type。Accept是客户端在请求中告诉服务端“我能够理解哪些格式”Content-Type是服务端在响应中告诉客户端“当前返回内容的实际格式是什么”。很多人在第一次接触时会把它们搞反实际上只要记住一个是“请给我这种格式”一个是“我给的是这种格式”。2.2 Accept 标头的工作过程当浏览器访问一个网页时请求头里一般带有Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,*/*;q0.8服务端看到text/html优先级最高于是返回普通 HTML。如果服务端同时实现了 HTML 和 Markdown 两种表示客户端可以把 Markdown 放在优先级最高的位置Accept: text/markdown, text/html;q0.9, */*;q0.1服务端根据这个请求头选择最匹配的媒体类型。如果text/markdown可用就返回Content-Type: text/markdown如果不可用就回退到text/html如果服务端一个都不支持理论上可以返回406 Not Acceptable但在实际项目中绝大多数服务端会选择忽略请求头并按默认格式返回这也是为什么很多接口即使不处理Accept也能工作。2.3 为什么选择 text/markdown 而不是 text/plainMarkdown 并不是一个“不正式”的媒体类型。RFC 7763 已经正式注册了text/markdown媒体类型并且定义了variant参数来区分 GitHub Flavored Markdown、CommonMark 等方言。因此在 HTTP 内容协商中使用text/markdown是合理的。为什么不直接用text/plain因为纯文本丢失了标题层级、列表、代码块、链接等结构AI 代理拿到后虽然也能读但分块、检索、引用来源都比 Markdown 麻烦。举个例子一段文档有多个二级标题Markdown 可以用##清晰地标记边界纯文本只能靠空行猜测。服务端返回 Markdown 的成本几乎为零但给下游带来的收益却很明显所以没有必要退回到text/plain。3. 面向 AI 代理的 Markdown API 设计方案3.1 设计目标给 AI 代理提供服务端 Markdown 内容不是简单地把一个 Markdown 文件扔出来就完事。真正的设计目标是同一个 URL 既能满足浏览器用户的 HTML 需求又能满足 AI 代理的 Markdown 需求代码改动尽量小内容安全可控缺少 Markdown 内容时有明确的降级策略。这里最关键的是“同一个 URL”。如果为了 AI 代理单独开一个/article.md路由虽然也能用但代理需要知道两套地址规则搜索引擎的sitemap也要重复维护。更好的做法是保留 URL让请求头决定返回哪种格式。这样浏览器输入 URL 时看到网页AI 代理只要加一个Accept标头就能拿到 Markdown对现有用户零影响。3.2 三种常见落地方式方式实现思路优点缺点A. 同一 URL 内容协商根据Accept返回 HTML 或 MarkdownURL 统一符合 HTTP 语义浏览器不受影响需要处理缓存Vary: AcceptB. 独立 Markdown 路由新增/markdown/{slug}或.md后缀路由后端逻辑简单直观URL 不统一需要额外维护地址映射C. 查询参数使用?formatmarkdown调试方便链接可预测不符合 REST 风格容易被缓存遗漏D. 自定义请求头例如X-Format: markdown简单直接绕过 HTTP 标准不够通用代理端需要专门设置从长期维护和 AI 生态兼容性来看推荐优先选择方式 A。如果现有系统已经有很多路由做一次通用的中间件改造就能覆盖所有页面。方式 C 可以作为调试辅助但不要作为主要方案因为查询参数会污染 URL也容易在不同组件之间传递时丢失。3.3 响应结构建议很多开发者遇到的问题是要不要把 Markdown 放进 JSON 里如果你做的是一个纯 API并且 AI 代理需要同时获得标题、作者、发布时间等元数据那么 JSON 包装是合理的例如{ title: 使用 Accept 标头向 AI 代理提供 Markdown 内容, published_at: 2025-01-01T10:00:00Z, content_markdown: ## 为什么 AI 代理需要 Markdown... }但如果你的目标是让现有网页在Accept: text/markdown下直接返回 Markdown 正文那么更推荐的响应格式是“裸 Markdown 文本”并且设置Content-Type: text/markdown; charsetutf-8。这样做的好处是AI 代理拿到响应后不需要再解析 JSON直接作为文档内容使用日志、过滤、压缩也更容易处理。如果确实需要元数据可以放到 HTTP 响应头中例如X-Document-Title而不是放入 JSON 包一层。4. 环境准备与前置条件4.1 后端技术选型本文会使用 Node.js 和 Python 各写一个最小示例。Node.js 示例使用 Express 框架Python 示例使用 FastAPI。这两个框架在社区中很常见代码也容易读懂。如果你使用的是 Java、Go、PHP核心思路完全一样判断请求头的Accept匹配text/markdown返回不同内容即可。环境要求Node.js 18npm 或者 pnpmPython 3.9pipcurl 命令行工具可选Nginx 1.20用于演示反向代理和缓存头配置具体版本请以你本机为准本文重点演示通用思路不绑定某个特定小版本。4.2 Node.js 项目初始化创建一个目录并初始化项目mkdir md-agent-demo cd md-agent-demo npm init -y npm install express4如果你的网络环境无法直接安装依赖可以改用本地离线依赖包但本文不展开。创建server.js文件后续代码都写在这个文件里。4.3 Python/FastAPI 项目初始化在另一个目录创建 Python 项目mkdir fastapi-md-demo cd fastapi-md-demo python -m venv .venv source .venv/bin/activate pip install fastapi uvicornWindows 环境激活虚拟环境命令是.venv\Scripts\activate。这里用 FastAPI 主要是看中它的依赖注入和自动文档能力实际生产环境也可以用 Flask 或 Django 实现同样的逻辑。5. 服务端实现根据 Accept 标头返回 Markdown5.1 Node.js/Express 示例在server.js中写入下面的代码// 文件路径md-agent-demo/server.js const express require(express); const app express(); const PORT 3000; const articleHtml !DOCTYPE html html head titleAI 友好内容/title meta charsetutf-8 / /head body headernav首页 | 关于 | 联系/nav/header article h1使用 Accept 标头向 AI 代理提供 Markdown 内容/h1 p这是一段正文讲述为什么 Markdown 更适合 AI 代理。/p h2核心观点/h2 ul liHTML 噪声多/li liMarkdown 结构清晰/li liAccept 标头可以实现内容协商/li /ul /article /body /html ; const articleMarkdown # 使用 Accept 标头向 AI 代理提供 Markdown 内容 这是一段正文讲述为什么 Markdown 更适合 AI 代理。 ## 核心观点 - HTML 噪声多 - Markdown 结构清晰 - Accept 标头可以实现内容协商 ; app.get(/article/1, (req, res) { const accept req.headers.accept || ; const wantsMarkdown accept.includes(text/markdown); if (wantsMarkdown) { res.set(Content-Type, text/markdown; charsetutf-8); res.send(articleMarkdown.trim()); return; } res.set(Content-Type, text/html; charsetutf-8); res.send(articleHtml); }); app.get(/health, (req, res) { res.json({ status: ok }); }); app.listen(PORT, () { console.log(Server running at http://localhost:${PORT}); });这里使用accept.includes(text/markdown)是一种简单判断方式。更严谨的做法是使用 Express 内置的req.accepts([text/html, text/markdown])方法它会考虑q权重和通配符。在实际项目中建议使用后者app.get(/article/1, (req, res) { const accepted req.accepts([text/html, text/markdown]); if (accepted text/markdown) { res.set(Content-Type, text/markdown; charsetutf-8); res.send(articleMarkdown.trim()); return; } if (accepted false) { res.status(406).send(Not Acceptable); return; } res.set(Content-Type, text/html; charsetutf-8); res.send(articleHtml); });req.accepts会解析请求头中的优先级。如果客户端明确表示只接受text/markdown而服务端不打算提供返回 406 是符合 HTTP 语义的。但对多数网页场景最稳妥的降级策略仍然是返回 HTML因为浏览器用户不会主动发送只接受 Markdown 的请求。5.2 Python/FastAPI 示例创建一个main.py文件# 文件路径fastapi-md-demo/main.py from fastapi import FastAPI, Request, Response from fastapi.responses import HTMLResponse, PlainTextResponse app FastAPI() MARKDOWN_CONTENT # 使用 Accept 标头向 AI 代理提供 Markdown 内容 这是一段正文讲述为什么 Markdown 更适合 AI 代理。 ## 核心观点 - HTML 噪声多 - Markdown 结构清晰 - Accept 标头可以实现内容协商 HTML_CONTENT !DOCTYPE html html headtitleAI 友好内容/titlemeta charsetutf-8/head body article h1使用 Accept 标头向 AI 代理提供 Markdown 内容/h1 p这是一段正文讲述为什么 Markdown 更适合 AI 代理。/p h2核心观点/h2 ul liHTML 噪声多/li liMarkdown 结构清晰/li liAccept 标头可以实现内容协商/li /ul /article /body /html app.get(/article/1) async def get_article(request: Request): accept request.headers.get(accept, ) if text/markdown in accept: return Response( contentMARKDOWN_CONTENT.strip(), media_typetext/markdown; charsetutf-8, ) return HTMLResponse(contentHTML_CONTENT) app.get(/health) async def health(): return {status: ok}FastAPI 的Response可以自由指定media_type所以返回 Markdown 和返回 HTML 一样简单。如果你不指定media_type默认会是text/plain这虽然也能用但下游代理可能会忽略结构信息。media_type里指定charsetutf-8能有效避免中文乱码。运行方式uvicorn main:app --host 0.0.0.0 --port 8000这里的media_typetext/markdown; charsetutf-8写法在 FastAPI 中是可行的。如果发现个别版本把分号解析异常也可以先设置media_typetext/markdown然后通过额外的响应头指定字符集。5.3 在 Nginx 层做缓存或回退很多生产环境会在服务前面加一层 Nginx。如果上游应用已经实现了内容协商Nginx 最重要的配置是加上Vary: Accept否则代理缓存可能会把 Markdown 响应缓存后发给浏览器或者反过来导致用户看到乱码或源码。这也是使用内容协商时最容易踩的坑。# 文件路径/etc/nginx/conf.d/md-agent.conf http { map $http_accept $backend { default http://html_backend; ~*markdown http://md_backend; } server { listen 80; server_name example.com; location /article/ { add_header Vary Accept; proxy_pass $backend; proxy_set_header Host $host; } } }这个配置的逻辑是根据Accept标头中是否包含markdown把请求转发到不同的上游服务。如果你的服务端本身已经能同时处理两种格式就不需要map只需要在响应头里输出Vary: Accept并让上游处理请求。注意Nginx 的if指令容易出现意料之外的继承问题更推荐使用map做这类分支判断。6. AI 代理端请求示例与效果验证6.1 使用 curl 验证启动 Node.js 或 FastAPI 服务后用 curl 直接验证curl -i -H Accept: text/markdown http://localhost:3000/article/1预期响应中应该包含HTTP/1.1 200 OK Content-Type: text/markdown; charsetutf-8响应体则是干净的 Markdown# 使用 Accept 标头向 AI 代理提供 Markdown 内容 这是一段正文讲述为什么 Markdown 更适合 AI 代理。 ## 核心观点 - HTML 噪声多 - Markdown 结构清晰 - Accept 标头可以实现内容协商如果不带Accept头或者使用浏览器的默认请求头应该返回 HTML。可以这样对比curl -i http://localhost:3000/article/1两者的Content-Type和响应体应该完全不同。这一步能让你确认内容协商真的生效了。6.2 在 AI 代理中设置请求头如果你的 AI 代理是一个 Python 脚本可以用requests库实现# requirements: requests import requests url https://example.com/article/1 headers { Accept: text/markdown, text/html;q0.9, */*;q0.1, } resp requests.get(url, headersheaders, timeout10) if resp.status_code 200 and text/markdown in resp.headers.get(content-type, ): content resp.text print(content) else: # 降级处理使用 HTML 转文本逻辑 content resp.text这段代码展示了 AI 代理的标准姿势先尝试拿 Markdown拿不到再降级。很多现成的“AI 代理助手加本地模型”方案本质就是把网页内容抓下来后喂给本地模型。如果抓取阶段得到的是干净的 Markdown本地模型在做摘要、问答、信息抽取时的输出质量会明显提高因为在提示词里不需要多余地强调“忽略导航和广告”。如果你使用的 AI 代理框架支持自定义请求头例如基于 LangChain、LlamaIndex 的网页加载器也可以看它是否暴露了headers参数。总的来说只要代理能够控制 HTTP 请求头就可以用这套机制。6.3 验证点验证是否成功可以按三个维度检查状态码正常应为 200如果出现 406说明服务端逻辑不够宽容优先让服务端降级到 HTML。Content-Type应该看到text/markdown; charsetutf-8而不是text/html或text/plain。内容质量响应体应该是带#、##、-、反引号等 Markdown 标记的正文而不是带着一堆div和script标签的源码。如果第三个验证点不通过说明服务端返回的只是“看起来像 Markdown 的纯文本”。虽然也能用但你应该检查是否所有标题、列表、代码块都被正确保留。AI 代理解析 Markdown 时靠的是这些结构标记。7. 常见问题与排查方法这里汇总了在生产环境中容易遇到的问题以及对应的排查思路。问题现象可能原因排查方式解决方案始终返回 HTML服务端没有写内容协商逻辑查看响应头和请求头是否包含text/markdown在路由中判断Accept并返回 Markdown返回 406 Not Acceptable服务端不支持请求的媒体类型且客户端不接受降级查看服务端代码的 accept 分支添加 HTML 回退不要轻易返回 406中文乱码响应缺少charsetutf-8检查 Content-Type 是否完整在media_type或Content-Type中指定 utf-8Markdown 代码块被丢失HTML 转 Markdown 时没有保留代码块语法用真实包含代码块的页面联调使用支持 GFM 的转换库服务端尽量返回原文件浏览器直接看到 Markdown 源码浏览器请求时把text/markdown排在最前检查浏览器插件或开发者工具是否修改 Accept在 nginx 或前端层区分普通浏览器请求和 AI 代理请求缓存响应互相污染没有设置Vary: Accept查看响应头是否包含 Vary在 Nginx 或应用层加上Vary: Accept代理拿到 Markdown 后链接不够完整Markdown 里只有相对链接检查转换规则服务端返回绝对 URL或提前做链接补全Markdown 被用户渲染后出现 XSS使用 Markdown 渲染器时没有过滤 HTML用含script的内容测试渲染前做 HTML 消毒禁用危险标签排查顺序建议是先确认请求头再确认响应头最后才看转换逻辑。很多时候问题不在代码而是某个中间层把Accept标头吞掉了。反向代理、API 网关、浏览器插件、爬虫框架都可能修改或丢弃请求头。你可以先在 curl 中直接请求源站逐步去掉中间层定位到具体是哪一层没有透传Accept。8. 最佳实践与工程建议8.1 降级策略不是所有页面都值得提供 Markdown 版本。首页、列表页、登录页通常结构复杂但 AI 代理并不特别需要真正有价值的是文章页、文档页、帮助中心、API 说明页。上线时可以先用一个路由前缀或页面类型来控制范围例如只对/docs/、/blog/、/help/开头的 URL 生效。对于没有 Markdown 内容的页面即使收到Accept: text/markdown也要返回 HTML让客户端自行决定下一步不要返回空白页更不要抛出 500 错误。8.2 安全与权限内容协商并不代表“任何格式都公开”。如果某篇付费文章、内部文档或需要登录才能访问的页面只允许 HTML 访问那么 Markdown 响应也必须执行相同的鉴权逻辑。否则AI 代理可能绕过原本为浏览器设计的界面限制直接拿到纯文本原文。反向代理层尤其要注意不能让/markdown/或.md后缀的 URL 成为鉴权盲区。文件存储的 Markdown 如果放在公开目录下会产生新的泄露路径。生产环境变更前应该在测试环境验证鉴权规则确认备份和回滚方案可用并且遵循最小权限原则只开放必要的路由。Markdown 内容本身也有注入风险。如果 AI 代理拿到 Markdown 后在本地用渲染器展示给用户Markdown 中的原始 HTML 标签、图片地址、链接可能引入 XSS 等问题。在服务端返回 Markdown 时可以过滤掉危险的script、iframe标签或使用支持 HTML 消毒的 Markdown 渲染库。8.3 下游工具链与本地模型Markdown 的生态已经足够成熟。开发者常用的 Typora、VS Code Markdown 插件、飞书文档等都能解析到不同层次的 Markdown甚至飞书已经支持渲染 Markdown 中的 Mermaid 流程图。对 AI 代理场景而言输出 Markdown 还有一个额外好处当你使用 SSE 流式输出模型回复时流式响应的每个 chunk 本身也是 Markdown 片段。如果这些片段最终要渲染到网页需要配套一个流式 Markdown 渲染器否则用户会看到不断闪烁的原始 Markdown 文本。如果你正在做一个“AI 代理助手加本地模型”的工具建议把内部文档、网页抓取结果、模型输出三段都统一成 Markdown 格式。这样配置文件、知识库切块、前端渲染、日志审计使用的是同一套规则可以省掉大量格式转换代码。很多团队一开始没有意识到这点等到需要把网页内容接入 RAG 时才发现清洗 HTML 的时间比写业务逻辑还长。8.4 性能与缓存内容协商对性能的影响很小真正要注意的是缓存。如果同一个 URL 可能返回两种不同格式那么缓存系统必须区分它们。HTTP 规范给出的答案是Vary: Accept。在 Nginx、CDN、浏览器缓存中只要响应头带上Vary: Accept缓存系统就会根据请求头区分两个版本避免串内容。如果你在服务端做 HTML 转 Markdown转换本身有一定的 CPU 成本。对于热点文章可以在写入文章时同时生成 Markdown 版本存到数据库或静态文件里请求时直接读取。这样内容协商就变成了简单的分支判断而不是每次请求都去解析 HTML。更重要的是给 Markdown 响应设置合适的Cache-Control例如public, max-age3600能有效降低源站压力。9. 总结与后续学习方向围绕“使用 Accept 标头向 AI 代理提供 Markdown 内容”这条主线这篇文章讲清楚了几个核心点HTML 对 AI 代理不友好Markdown 是更合适的内容表示HTTP 的 Accept 标头和内容协商机制并不复杂关键在于服务端和代理端都要支持用 Node.js 或 FastAPI 实现一个 Markdown 响应分支只花几分钟真正容易出错的是缓存、鉴权和降级策略而不是代码本身。建议你接下来做一个最小实验先用一个本地 Express 服务把一篇文章分别以 HTML 和 Markdown 返回然后用 curl 验证两种请求结果接着写一个 Python 脚本模拟 AI 代理通过Accept: text/markdown拉取内容并喂给本地模型观察摘要质量。如果你有博客或文档站也可以考虑给它加上这个标头支持配合Vary: Accept上线后观察流量变化和日志你会看到 AI 代理的抓取成功率明显比过去靠 HTML 清洗时要稳定。