HTTP内容协商实践:利用Accept标头为AI代理输出Markdown 这次我们来看一个很容易被忽略、但对 AI 代理和内容服务都很重要的 HTTP 细节通过 Accept 标头向 AI 代理提供 Markdown 内容。如果你写了一个内容网站、知识库、文档站或者一个工具型 API而最近在接各种 AI 代理、大模型抓取、RAG 检索流程大概率会遇到同一个尴尬AI 代理拿到的是满满的 HTML 标签正文混杂在 div、span、script 和导航菜单里解析效率低、回答准确率差。其实不必把模型换成更大参数的也不需要把页面改成纯文本只需要在 HTTP 响应里根据Accept标头返回干净的 Markdown 内容问题就能解决一大半。这篇文章会讨论以下几个核心问题Accept标头在 AI 代理场景下到底怎么用后端接口如何实现“请求 HTML 返回 HTML请求 Markdown 返回 Markdown”的内容协商Nginx 反向代理层怎么做兜底转换用 curl、Python、Node.js 如何验证 AI 代理的 Markdown 请求批量抓取、接口调用、缓存和性能观察的通用套路常见问题排查清单和最佳实践。适合正在做内容型 API、文档站、知识库检索或者给自己的 AI Agent 工具链加“网页转干净文本”能力的开发者阅读。1. 核心能力速览能力项说明功能定位通过 HTTP Accept 标头进行内容协商让 AI 代理或普通客户端按需获取 Markdown 或 HTML核心机制服务端检查请求头Accept: text/markdown决定返回 Markdown 原文或 HTML适用对象内容站、文档站、博客、知识库、API 服务、RAG 管道、AI 代理浏览器兼容浏览器通常发送 HTML 相关 Accept不影响普通用户访问AI 代理友好减少 HTML 标签干扰降低 token 消耗提高上下文利用率实现方式后端原生生成、反向代理转换、静态站点预生成批量任务可配合批量抓取、内容定时同步、RAG 索引更新接口能力同一 URL 可同时服务浏览器、App、AI 代理门槛不需要 GPU/显存普通服务器即可运行核心风险内容版权、访问控制、缓存策略、请求来源校验从能力上看这个方向并不是一个复杂的“大模型项目”而是一个偏工程化的内容交付优化方案。只要后端能拿到 Markdown 原文就能用很小的成本把内容贡献给 AI 生态。2. 为什么 AI 代理需要 Markdown先看一个典型场景。AI 代理爬取一个文档页面时服务端返回的 HTML 可能是这样的!DOCTYPE html html head titleAPI 参考/title /head body nav首页 / 文档 / API 参考 / 鉴权接口/nav div classsidebar a href#快速开始/a a href#参数说明/a a href#错误码/a /div div classcontent p调用该接口时需要在请求头中携带 codeAuthorization: Bearer TOKEN/code。/p script // 无关的统计代码 /script button classcopy-btn复制/button /div /body /html如果是给浏览器看这个结构没问题。但 AI 代理在处理时会把这些标签、导航、脚本全部当作上下文读进去。一方面浪费 token另一方面模型很容易被无关内容干扰导致最终回答不准确。如果同样的 URL 在请求时带上Accept: text/markdown服务端返回的是# API 参考 调用该接口时需要在请求头中携带 Authorization: Bearer TOKEN。这个文本干净、结构明确、token 占用低对 AI 代理非常友好。所以“用 Accept 标头提供 Markdown 内容”本质上是在做面向不同客户端的内容协商让同一份内容以不同格式输出。常规 Web 内容协商常见的媒体类型有Accept 值典型返回格式text/htmlHTML 页面application/jsonJSON 数据text/markdownMarkdown 原文text/plain纯文本application/rssxmlRSS 订阅内容当浏览器访问时请求头通常包含text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8所以优先级仍是 HTML。当 AI 代理访问时只要代理设置了Accept: text/markdown服务端就可以走 Markdown 分支。3. 适用场景与使用边界3.1 适合谁对外提供文档、博客、帮助中心的内容型站点提供 API 查询接口希望返回结构化内容的服务正在搭建 RAG 检索管道需要把网页转成干净文本的开发团队想让自己的 Agent、脚本、内部工具更稳定读取网站内容的个人开发者希望内容被主流 AI 搜索、AI 代理更容易引用的站点运营者。3.2 能解决什么问题避免 HTML 标签、CSS、JavaScript 进入上下文降低 token 消耗提高 AI 代理解析页面的准确率保留 Markdown 中的标题、列表、表格、代码块结构为同一个内容同时服务浏览器、移动端、App 和 AI 代理。3.3 不适合什么场景页面依赖复杂 JavaScript 动态渲染且服务端拿不到 Markdown 原文不适合单纯靠 Accept 头解决不需要二次加工只要给 AI 喂原始 HTML 的简单抓取不需要做如果页面内容多是图片、视频、交互组件Markdown 反而会丢失信息不适合。3.4 使用边界与合规提醒任何内容交付能力都必须注意版权、访问控制和隐私边界如果你的站点内容来自他人作品通过 Markdown 接口对外提供时需要确认授权范围接口不能绕过登录、付费墙、访问控制如果站点不希望被 AI 代理抓取要通过 robots 协议或访问白名单做好限制涉及个人隐私信息的内容必须做权限校验给 AI 代理提供 Markdown 不等于放弃版权你需要明确许可证和引用规则。4. 环境准备与前置条件这一方案不需要 GPU 和专用硬件普通 Linux 虚拟机或云服务器就行。以下是一套通用环境清单具体版本以你实际项目为准。4.1 基础环境清单项目建议操作系统LinuxUbuntu/Debian/CentOS或 macOS开发语言Python 3.9 / Node.js 18Web 框架FastAPI、Flask、Express 等反向代理Nginx 或 Caddy可选Markdown 处理库Python 的markdown、Node 的marked、markdown-it缓存服务Redis 或文件缓存可选测试工具curl、Postman、Python requests、Node fetch4.2 不需要准备的不需要 GPU不需要大显存不需要大模型本地部署不需要复杂的向量数据库。所以这个方案更适合作为内容服务的一层“格式化出口”而不是独立的 AI 推理服务。4.3 服务端需要什么服务端要能拿到 Markdown 原文。通常有三种来源数据库里保存了 Markdown 原文静态文件是.md只需要按请求头返回文件内容内容本身是 HTML需要先把 HTML 转成 Markdown再返回。第一条路径最干净也是本文重点。第三条路径也有可行方案在“Nginx 兜底转换”部分会说明。5. 后端实现按 Accept 标头返回 Markdown我们先写一个最小可运行的后端示例。使用 FastAPI核心逻辑就是读取请求头判断Accept是否包含text/markdown然后返回不同格式。5.1 定义一条测试数据为方便演示先准备一份 Markdown 原文# 欢迎使用示例 API 本接口用于演示 **Accept 内容协商**。 ## 功能列表 - 支持 HTML - 支持 Markdown - 支持 JSON5.2 FastAPI 实现from fastapi import FastAPI, Request, Response from fastapi.responses import HTMLResponse, PlainTextResponse app FastAPI(titleAccept Markdown Demo) MARKDOWN_CONTENT # 欢迎使用示例 API 本接口用于演示 **Accept 内容协商**。 ## 功能列表 - 支持 HTML - 支持 Markdown - 支持 JSON HTML_TEMPLATE !DOCTYPE html html headtitle欢迎使用示例 API/title/head body h1欢迎使用示例 API/h1 p本接口用于演示 strongAccept 内容协商/strong。/p h2功能列表/h2 ul li支持 HTML/li li支持 Markdown/li li支持 JSON/li /ul /body /html app.get(/article) async def get_article(request: Request): accept request.headers.get(accept, ) # AI 代理请求 Markdown if text/markdown in accept: return Response( contentMARKDOWN_CONTENT, media_typetext/markdown; charsetutf-8 ) # 普通浏览器请求 HTML if text/html in accept: return HTMLResponse(contentHTML_TEMPLATE) # 其他客户端默认返回 Markdown或按需返回 JSON return PlainTextResponse(contentMARKDOWN_CONTENT)启动服务uvicorn main:app --host 0.0.0.0 --port 8000这个示例的核心点并不复杂关键是同一个 URL不同 Accept返回不同响应。5.3 Express 实现如果你使用的是 Node.js实现思路一样const express require(express); const app express(); const markdown # 欢迎使用示例 API 本接口用于演示 **Accept 内容协商**。 ; app.get(/article, (req, res) { const accept req.headers.accept || ; if (accept.includes(text/markdown)) { res.setHeader(Content-Type, text/markdown; charsetutf-8); return res.send(markdown); } res.setHeader(Content-Type, text/html; charsetutf-8); res.send(h1欢迎使用示例 API/h1p普通 HTML 内容/p); }); app.listen(3000, () { console.log(Server is running on http://localhost:3000); });整体上语言框架不影响方案设计核心就是“内容协商”。6. Nginx 反向代理层的 Markdown 兜底方案不是所有旧服务都能改代码。很多时候服务已经返回 HTML但我们希望 AI 代理拿到 Markdown。这时候可以在 Nginx 层做两件事检测Accept: text/markdown将该请求转发到一个转换服务或静态 Markdown 服务。6.1 简单按 URL 分流如果站点本来就有.md文件可以用 Nginx 直接返回静态 Markdownserver { listen 80; server_name example.com; location /docs/ { if ($http_accept ~* text/markdown) { rewrite ^/docs/(.*)$ /markdown/$1.md break; proxy_pass http://127.0.0.1:8000; } proxy_pass http://127.0.0.1:3000; } }注意上面的if指令在实际 Nginx 配置中不是最优雅的写法但作为小型演示可跑通。生产环境更推荐用map加proxy_set_header或者交给应用层处理。6.2 使用独立转换服务如果你希望自动把 HTML 转成 Markdown可以在 Nginx 后面挂一个小服务专门接收需要转换的页面 URL然后返回转换结果。curl -X POST http://127.0.0.1:9000/convert \ -H Content-Type: application/json \ -d {url: http://localhost:8000/article}转换服务内部可用readability、html-to-markdown、trafilatura等工具完成。这里给出一个 Python 通用思路import requests from bs4 import BeautifulSoup import html2text def fetch_and_convert(url: str) - str: resp requests.get(url, timeout10) soup BeautifulSoup(resp.text, html.parser) # 提取主内容通常需要根据站点结构调整选择器 article soup.select_one(article) or soup.body h html2text.HTML2Text() h.ignore_links False return h.handle(str(article))这种“Nginx 检测 Accept - 转发转换服务 - 返回 Markdown”的链路适合有多套老站点、不想大规模改代码的情况。7. 功能测试与效果验证7.1 验证目标携带Accept: text/markdown时能拿到 Markdown普通浏览器请求时仍返回 HTML不携带 Accept 或携带*/*时行为可预期响应头Content-Type正确。7.2 用 curl 测试先启动 FastAPI 服务然后在终端执行# 模拟 AI 代理请求 Markdown curl -s -H Accept: text/markdown http://127.0.0.1:8000/article预期返回# 欢迎使用示例 API 本接口用于演示 **Accept 内容协商**。 ## 功能列表 - 支持 HTML - 支持 Markdown - 支持 JSON再模拟浏览器请求curl -s -H Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8 http://127.0.0.1:8000/article预期返回 HTML 页面。7.3 用 Python 验证接口import requests url http://127.0.0.1:8000/article resp_markdown requests.get(url, headers{Accept: text/markdown}) print(状态码:, resp_markdown.status_code) print(Content-Type:, resp_markdown.headers.get(content-type)) print(resp_markdown.text)验证点状态码是否为200Content-Type是否包含text/markdown正文是否以#开头不包含div、nav等标签。7.4 判断测试是否成功的标准检查项通过条件Accept 为 text/markdown返回 MarkdownAccept 为 text/html返回 HTML无 Accept返回默认格式建议返回 MarkdownContent-Type与返回内容一致状态码200响应体中无 HTML 标签Markdown 模式下不应出现div等中文编码charsetutf-8无乱码7.5 常见失败原因问题现象可能原因排查方式拿到的是 HTML后端没读取 Accept 头打印请求头确认返回 406媒体类型不匹配检查 Content-Type 和返回类型Markdown 中文乱码缺少 charsetutf-8设置 UTF-8浏览器也拿到 Markdown判断逻辑反了检查if分支优先级动态页面拿到空内容页面靠 JS 渲染改用无头浏览器或静态导出8. 接口 API 与批量任务兼容8.1 通用 API 响应设计如果你在做一个内容型接口建议把 Accept 内容协商当成基础能力而不是只给 AI 代理开一个独立路由。这样可以保持 URL 统一方便缓存和权限配置。一个通用响应流程是请求进入 - 鉴权 - 读取 Accept - 判断 text/markdown - 返回 Markdown / HTML / JSON8.2 同一个 URL 返回多种格式可以设计成# Markdown curl -H Accept: text/markdown https://api.example.com/articles/123 # HTML curl -H Accept: text/html https://api.example.com/articles/123 # JSON curl -H Accept: application/json https://api.example.com/articles/123这里的参数、路径、返回结构需要按照实际业务调整核心是“同一个资源不同表现层”。8.3 批量任务与抓取批量任务主要有两种批量抓取网站内容并转成 Markdownimport time import requests urls [ https://example.com/docs/guide, https://example.com/docs/api, ] results {} for url in urls: try: resp requests.get( url, headers{Accept: text/markdown}, timeout10 ) resp.raise_for_status() results[url] resp.text except Exception as e: results[url] fERROR: {e} # 控制请求频率避免对目标站点造成压力 time.sleep(1) print(results)存量内容批量导出如果后端数据库里已经有大量 Markdown 内容你可以写一个定时任务把所有文章的 Markdown 原文导出到指定目录方便 RAG 管道索引。# 示例导出命令按实际情况调整 python scripts/export_markdown.py --output ./exports批量任务建议加三个机制每个 URL 单独记录成功/失败失败自动重试 1 到 2 次写入日志方便定位是网络问题、超时问题还是内容问题。8.4 接口权限控制因为 Markdown 输出会让内容更容易被复制所以有条件的情况下建议对站内 API 路由做签名校验只允许指定 User-Agent 或 IP 访问 Markdown 接口对高频抓取做限流对非公开内容校验登录态。9. 资源占用与性能观察9.1 为什么这个方案很轻如果 Markdown 原文已经存在服务端只是根据请求头返回字符串性能开销几乎可以忽略。主要资源消耗在于并发请求连接数网络带宽如果有动态转换CPU 消耗会略高。9.2 如何观察资源占用在 Linux 服务上可以直接用top或pidstat观察进程 CPU 占用top -p PID也可以记录接口响应时间time curl -s -H Accept: text/markdown http://127.0.0.1:8000/article -o /dev/null如果响应时间明显增加优先检查数据库查询、HTML 转换逻辑和网络带宽而不是盲目加内存。9.3 降低转换开销的办法优先返回原文 Markdown避免每次请求都做 HTML to Markdown 转换对转换结果做缓存对静态页面启用 CDN 缓存不同 Accept 加不同缓存键大批量导出时使用异步任务队列而不是同步请求。9.4 缓存键设计如果你使用了缓存必须把Accept纳入缓存键例如cache_key f{url}:{accept_type}否则会出现“浏览器缓存了 HTMLAI 代理拿不到 Markdown”的问题。10. 常见问题与排查方法问题现象可能原因排查方式解决方案请求Accept: text/markdown却返回 HTML后端未读取 Accept或判断分支错误打印请求头确认服务端收到的 Accept 值在返回前检查 Accept 匹配逻辑AI 代理获取到 Markdown 但格式混乱服务端返回了 HTML 转 Markdown 的脏结果检查源 HTML 布局和选择器使用更准确的正文提取策略浏览器访问内容样式丢失错误地将浏览器请求也返回了 Markdown检查 Accept 优先级浏览器 Accept 不要匹配 text/markdown中文乱码Content-Type 缺少 charset观察响应头设置为text/markdown; charsetutf-8接口 406未实现text/markdown类型查看后端异常添加对应响应类型处理CDN 缓存了 HTML缓存键未包含 Accept检查 CDN 配置增加 Vary 头批量抓取间歇性失败目标站限流或超时查看错误日志增加重试和限速页面内容是 JS 渲染服务端没有 Markdown 原文检查 curl 返回改用无头浏览器或预渲染10.1 一个特别值得注意的点很多网站在接入 CDN 或 Nginx 缓存时不会设置Vary: Accept。结果就是第一个请求如果返回的是 HTML后续所有带Accept: text/markdown的请求也可能命中 HTML 缓存。要在缓存层加入add_header Vary Accept;这句话的目的是告诉缓存系统Accept头不同缓存版本不同。否则AI 代理很可能一直拿到 HTML。11. 最佳实践与使用建议11.1 第一次先做最小验证不要一上来就改造所有页面。先挑一个接口或一条静态文档实现 Markdown 返回然后用 curl 验证再接入自己的 AI Agent。跑通一条链路后再扩展其他路径。11.2 保留一套最小可运行配置建议把“Accept 判断”单独封装成一个函数或中间件避免每个接口都写重复逻辑。示例def should_return_markdown(accept: str) - bool: return text/markdown in acceptfunction shouldReturnMarkdown(accept) { return accept.includes(text/markdown); }11.3 模型文件、输入素材、输出结果分目录管理这里虽然是内容接口但同样适用目录管理思路project/ ├── app/ │ ├── main.py │ └── routes/ ├── content/ │ ├── markdown/ │ └── html/ ├── exports/ └── logs/Markdown 原文、转换脚本、导出结果分开管理后期维护会轻松很多。11.4 批量任务要加日志和失败重试批量任务最容易出现“跑到一半不知道卡在哪个 URL”。建议每处理一条就写一行日志import logging logging.info(fetching %s, status%s, url, resp.status_code)失败重试建议使用指数退避import time for retry in range(3): try: resp requests.get(url, headersheaders, timeout10) break except Exception: time.sleep(2 ** retry)11.5 接口服务要限制访问范围Markdown 接口本质上是把内容以更容易复制的方式送给外部客户端所以必须配合鉴权和限流。至少设置允许的 API Key用户代理白名单每 IP 每分钟请求数限制非公开内容必须带登录态校验。11.6 涉及人脸、声音、版权素材时必须确认授权如果你的站点包含图片、音频、视频或其他版权素材Markdown 输出可能不会包含这些二进制文件但内容本身仍可能涉及版权。对外提供 Markdown 之前要确认内容是否有转载和分发权限是否包含第三方素材是否允许被 AI 代理引用和生成衍生内容是否需要对 AI 代理单独设定 robots 或访问规则。11.7 发布或商用前要做效果复核给 AI 代理提供 Markdown 之后不要直接上线。建议先自己用几个主流 AI 代理或抓取工具实际访问一下检查标题是否正确层级是否清晰代码块是否完整表格是否保留链接是否可用是否漏掉了关键信息。12. 后续可以继续扩展的方向12.1 与 RAG 检索集成可以在 Markdown 接口上直接接 RAG 管道把返回内容切分成 block再做向量化存储。这样每个文档 URL 都对应一份干净的检索单元。12.2 与静态站点生成器集成如果你使用 Astro、VitePress、Docusaurus 等静态站点工具构建产物里通常同时包含.html和.md。可以通过 Nginx 或 CDN 规则让带Accept: text/markdown的请求直接映射到.md文件。12.3 与 AI 搜索增强集成当 AI 代理引用站点内容时如果返回的 Markdown 里带有原文链接、发布时间、作者信息会更容易被引用和溯源。可以在 Markdown 顶部自动生成元信息--- title: 欢迎使用示例 API url: https://example.com/article published_at: 2025-01-01 ---12.4 按客户端定制 Markdown除了内容协商还可以根据 User-Agent 进一步判断是哪种 AI 代理返回不同详细程度的内容。比如普通用户访问直接返回 Markdown 原文搜索引擎返回更规范的头信息和目录内部 Agent返回带调试信息的 Markdown。不过这个建议要谨慎不要因此造成内容不一致或权限绕过。如果你正在做内容型服务我的建议是尽早把Accept: text/markdown支持加上。它不需要大模型不需要复杂框架可能只是几十行代码但当你自己的 AI 代理、RAG 管道和外部 AI 搜索访问站点时返回干净的结构化文本带来的体验提升会非常明显。可以从一个curl命令开始先验证自己的服务能不能按 Markdown 返回内容再决定要不要接入缓存、批量导出和 RAG 流程。这个方向值得收藏备查。