使用Accept标头让AI代理直接获取Markdown内容 当 AI 代理需要阅读网页内容时返回 HTML 往往会让它非常“难受”标签噪声多、正文提取困难、上下文被截断。而 Markdown 是一种结构清晰、语义密度高、容易被大模型解析的纯文本格式。本文从 HTTP 的 Accept 标头入手讲解如何让服务端根据客户端期望自动返回 Markdown 内容并给出可复现的 Flask / FastAPI 示例、常见坑点和工程建议。之前在做知识库对接时我的服务端接口一直被 AI 代理“嫌弃”返回 HTML 页面后代理要么把div、style这些标签当成正文要么因为 token 浪费太多导致回答质量下降。后来调研发现很多团队在对外提供内容时已经不再只针对“浏览器”这一个客户端而是把“AI 代理”也当成了头号用户。而 AI 代理最理想的内容格式往往就是 Markdown。这篇文章会围绕“使用 Accept 标头向 AI 代理提供 Markdown 内容”这一主题拆解 HTTP 内容协商原理、Markdown 的 AI 友好特性、服务端实现方案以及实际项目中常见的错误排查和最佳实践。如果你正在做 API 服务、AI 应用、知识库工具或者希望自己开发的站点能更好地被 AI 代理消费这篇文章值得你收藏反复看。1. 背景与核心概念AI 代理为什么需要 Markdown1.1 一个熟悉的开发场景先想象一个很常见的链路用户启动一个 AI 助手问它“帮我总结一下企业官网上的产品更新”。AI 代理接到任务后会主动请求你提供的网页地址把内容放进上下文然后交给大模型总结。这里就出现了一个非常现实的问题AI 代理请求一个 URL 时得到的往往是完整的 HTML 页面。HTML 里面不仅有正文还有导航栏、页脚、广告位、CSS 样式、JS 脚本。为了让大模型“看懂”页面代理必须先从 HTML 里抽取正文再把正文转成更适合理解的纯文本。这个过程容易出错而且非常浪费 token。如果服务端在响应请求时能识别“这是一个 AI 代理在访问”并且直接返回干净的 Markdown 内容那么代理就不需要做复杂解析了上下文更短理解效果也更好。这就是“使用 Accept 标头向 AI 代理提供 Markdown 内容”这一方案的核心价值。1.2 Accept 标头到底是什么Accept 是 HTTP 请求头中的一个标准字段它用于告诉服务端客户端希望收到什么媒体类型的数据。比如浏览器请求页面时通常会发送Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,*/*;q0.8这行信息的意思是浏览器最想要 HTML其次是 XML其他类型也可以接受但优先级比较低。AI 代理在发起请求时可以通过 Accept 头声明自己想要 Markdown。例如Accept: text/markdown, text/plain;q0.8, text/html;q0.5服务端看到这个请求头后如果自己也支持text/markdown就可以在响应中直接把 Content-Type 设为text/markdown并且响应体直接输出 Markdown 文本。这就是一次非常标准的内容协商。1.3 内容协商与 AI 代理的天然组合HTTP 内容协商Content Negotiation听起来很专业其实本质很简单服务端和客户端商量一下大家都支持哪种格式最后选一个两边都满意、效率也最高的格式返回。过去内容协商主要用于浏览器和服务器之间达到“同一个 URL不同设备返回不同版本”的效果。现在 AI 代理大量出现之后这种协商能力被赋予了新的用途当代理想要 Markdown 时服务端就能提供 Markdown当普通浏览器访问同一个 URL 时服务端仍然返回排版好的 HTML 页面。同一个接口服务两种完全不同的客户端这恰恰是内容协商的经典应用场景。理解这一点之后接下来的问题就变成了如何在服务端判断客户端想要什么以及如何用代码把正确的格式返回出去。2. 环境准备与示例工程结构2.1 运行环境说明在写代码之前先说明一下本文使用的运行环境。版本号在不同项目中会有差异本文示例以常见稳定环境为例重点演示配置思路大家需要根据自己本机的实际情况做微调。操作系统Windows 10 / macOS / Linux 均可Python3.9 或更高版本Web 框架Flask 2.x 或 FastAPI 0.100依赖库Flask、Python-Markdown、requests请求测试工具curl 或 Postman / Apifox代码编辑器VS Code 或 PyCharm需要注意这里的 Python-Markdown 并不是一个必需的依赖。如果你只是返回纯 Markdown 文本不一定需要安装它。但如果你想要“把内部 HTML 文档转成 Markdown 再返回”或者“把业务数据渲染成带格式的 Markdown”就需要这个库来辅助生成。2.2 项目结构为了演示完整我设计了一个非常小的示例项目结构如下markdown-agent-demo/ ├── app.py # Flask 服务端主文件 ├── fastapi_app.py # FastAPI 版本可选 ├── requirements.txt # 依赖清单 ├── templates/ │ └── article.html # 普通浏览器访问时的 HTML 模板 ├── content/ │ ├── article_1.md # 模拟 Markdown 原始内容 │ └── article_1.html # 模拟 HTML 原始内容 └── client/ ├── test_curl.sh # curl 测试脚本 └── agent_request.py # 模拟 AI 代理请求 Python 脚本这个结构并不复杂。核心逻辑集中在app.py里你只需要关注它其他文件都是为了辅助演示。2.3 初始化虚拟环境与安装依赖为了不让第三方库污染系统级的 Python 环境建议先创建虚拟环境。python3 -m venv venv source venv/bin/activate # Windows 使用 venv\Scripts\activate接着创建requirements.txtflask2.0 markdown3.4 requests2.31安装依赖pip install -r requirements.txt安装完成后可以运行pip list确认依赖是否已经就绪。3. Accept 标头语法与内容协商机制拆解3.1 Accept 标头的格式Accept 标头的格式用一句话概括一个或多个“媒体类型 优先级”的组合。优先级通过q参数表示取值范围是 0 到 1没有写q值时默认是1。例如Accept: text/html, application/json;q0.9, */*;q0.1含义如下text/html优先级最高默认 q1application/json;q0.9如果服务端没有 HTML可以返回 JSON 作为替代*/*;q0.1其他类型都可以接受但优先级非常低这里要特别注意的是q值越小优先级越低。多个媒体类型之间用英文逗号分隔而每个媒体类型内部如果需要扩展参数则用分号分割。在服务端开发人员通常不会手写解析 Accept 字符串而是使用框架提供的能力。例如 Flask 里的request.accept_mimetypes已经帮你完成了优先级排序。FastAPI 的Request对象也能拿到原始请求头但判断逻辑通常需要自己封装。3.2 服务端如何判断并选择类型服务端判断流程可以拆成三步。第一步读取Accept请求头。大部分 Web 框架都能直接拿到原始字符串。第二步按优先级从高到低尝试匹配。比如客户端最想要text/markdown服务端发现自己确实支持这种做法就返回 Markdown如果不支持再降级去找text/plain再不支持就返回 HTML 或 JSON。第三步设置对应的Content-Type响应头。例如返回 Markdown 时Content-Type 应为text/markdown; charsetutf-8返回 HTML 时Content-Type 应为text/html; charsetutf-8。关键逻辑看似简单但实践中有个细节有些框架自带内容协商有些框架把 raw 请求头直接交给你处理。如果你发现自己设置的头没有生效多半是框架之前已经定好了响应对象类型需要去覆盖它。3.3 Accept、Content-Type、Accept-Encoding 三者的区别很多初学者会把这几个 HTTP 头搞混这里我用一张表格把它们的职责划分清楚。请求头/响应头方向作用示例值Accept请求头客户端声明自己想要什么格式Accept: text/markdownContent-Type响应头也可能是请求头服务端告诉客户端“我返回的数据是什么格式”Content-Type: text/markdown; charsetutf-8Accept-Encoding请求头客户端声明支持哪些压缩算法Accept-Encoding: gzip, brContent-Encoding响应头服务端告诉客户端响应数据使用了哪种压缩Content-Encoding: gzip可以发现Accept 和 Content-Type 是“商量”的关系。客户端想要 Markdown服务端确认后返回 Markdown并在 Content-Type 里标明text/markdown。如果服务端做不到也可以降级返回 HTML 或 JSON但这时就要接受 AI 代理解析成本变高的事实。而 Accept-Encoding 和 Content-Encoding 处理的是另一件事传输体积优化。两者互不干扰一个解决“语言格式问题”一个解决“传输压缩问题”。4. Markdown 的 AI 友好特性与格式约定4.1 为什么大模型更喜欢 Markdown从数据角度看大模型训练语料中Markdown、纯文本、代码文件占了相当大的比例。Markdown 本身是纯文本结构却包含标题、列表、引用、代码块、表格等丰富语义这比“没有结构的纯文本”更容易被模型理解又比 HTML 少了大量标签噪声。举个例子。同样是表达一个三级列表HTML 可能是ul li第一项/li li第二项/li /ul而 Markdown 是- 第一项 - 第二项AI 代理读取后者时token 消耗更少语义也更直接。更重要的是很多知名 AI 知识库和 RAG 系统在文档解析阶段都会把 HTML 转成 Markdown 之后再做切片存储。如果你在 HTTP 接口层就能直接输出 Markdown等于帮下游省去了一个转换步骤。4.2 AI 代理消费 Markdown 的三种典型模式AI 代理消费 Markdown 的方式目前主要可以分为三种。第一种是抓取网页后转为 Markdown。这是最常见的方式代理访问 URL拿到 HTML然后调用类似html2text或trafilatura的库抽取正文并转成 Markdown。这种方式的问题是处理规则复杂网站改版后容易失效。第二种是服务端直接返回 Markdown。这就是本文的实践方向。服务端根据 Accept 头判断客户端是否支持 Markdown如果支持直接返回text/markdown。这种方式对 AI 代理最友好处理成本最低。第三种是通过 API 显式指定格式。比如 API 路径中带?formatmarkdown或者请求体里声明想要 Markdown。这种方式实现简单、可控性强但相比 Accept 头不够“自动化”。如果你同时支持这两种方式一般建议让 Accept 头优先查询参数作为显式覆盖。4.3 从 HTML 到 Markdown 的转换注意点很多团队初期没有考虑“AI 代理要访问”这件事线上内容已经全部是 HTML。这时候给 AI 代理提供 Markdown就需要一个转换能力。常见的 Python 转换方案有两种。一是使用html2text库优点是方便快捷一次调用就能拿到比较干净的 Markdown二是使用trafilatura它更适合网页正文抽取能过滤导航、页脚和广告区块。如果你是在 Flask 服务里对已存好的 HTML 文件做转换html2text就够了。转换时有两个注意点。第一图片相对路径问题。HTML 里如果图片是相对路径转成 Markdown 后要补全为绝对 URL否则 AI 代理无法正确加载图片地址。第二代码块语言标注。有些 HTML 中代码块缺少language类转换后可能无法自动保留语言类型。如果内容里大量涉及代码建议转换后检查一下 代码块的标注是否完整。5. 完整实战让 API 通过 Accept 标头返回 Markdown5.1 服务端实现Flask 内容分发接口下面我们用一个 Flask 应用来做完整演示。这个应用对外提供一个文章接口接口会根据客户端的 Accept 头自动返回 Markdown 或 HTML。先创建content/article_1.md用来模拟后台已有的 Markdown 原始内容# 一文搞懂 Accept 标头与 AI 代理 在 HTTP 协议中Accept 标头是内容协商的起点。 ## 为什么 AI 代理需要 Markdown - Markdown 结构清晰 - 标签噪声少 - token 消耗相对更低 ## 核心要点 | 场景 | 推荐格式 | | --- | --- | | 浏览器访问 | text/html | | AI 代理访问 | text/markdown |再创建app.py# 文件路径markdown-agent-demo/app.py from flask import Flask, request, abort, Response import markdown as md_lib import pathlib app Flask(__name__) CONTENT_DIR pathlib.Path(content) def load_markdown_content(article_id: str) - str: 从 content 目录下读取对应的 Markdown 文件。 这里为了演示目录名和文件名都使用 article_id 来索引。 md_file CONTENT_DIR / f{article_id}.md if not md_file.exists(): abort(404, descriptionArticle not found) return md_file.read_text(encodingutf-8) def render_html_from_markdown(md_text: str) - str: 将 Markdown 渲染为 HTML。 如果希望更美观可以套一层模板这里返回一个完整 HTML 片段。 body_html md_lib.markdown( md_text, extensions[tables, fenced_code, codehilite], output_formathtml5, ) return f !DOCTYPE html html langzh-CN head meta charsetutf-8 titleArticle/title /head body article{body_html}/article /body /html app.route(/articles/article_id) def article_detail(article_id: str): # 1. 读取 Markdown 原始内容 md_text load_markdown_content(article_id) # 2. 通过 Flask 框架内置的 accept_mimetypes 判断客户端偏好 accept request.accept_mimetypes # 3. 如果客户端明确表示接受 text/markdown并且优先级高于 text/html # 则直接返回 Markdown 原始内容 # 这里用 best_match 可以避免自己手写优先级比较逻辑 best_match accept.best_match([text/markdown, text/html, text/plain]) if best_match text/markdown: return Response( md_text, status200, mimetypetext/markdown, headers{X-Content-Source: markdown-original}, ) if best_match text/html: html_content render_html_from_markdown(md_text) return Response( html_content, status200, mimetypetext/html, headers{X-Content-Source: html-rendered}, ) # 4. 如果客户端接受的类型不明确默认返回 HTML避免影响普通浏览器体验 return Response( render_html_from_markdown(md_text), status200, mimetypetext/html, headers{X-Content-Source: html-fallback}, ) if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)这段代码的核心逻辑在article_detail函数中。我使用了 Flask 的request.accept_mimetypes.best_match方法它会在你传入的候选列表中自动挑选客户端最偏好的一种媒体类型。比如客户端发送Accept: text/markdown那么best_match就会返回text/markdown于是服务端直接把 Markdown 原样返回。需要注意best_match并不只是看Accept头里有没有某个值它还会参考q优先级。比如客户端发送Accept: text/html;q1.0, text/markdown;q0.5那么best_match会优先返回text/html因为 HTML 的优先级更高。5.2 代码说明为什么使用 mimetype 而不是直接改 Content-Type有读者可能会问为什么不直接写response.headers[Content-Type] text/markdown在 Flask 中Response对象的mimetype参数会负责生成正确的Content-Type响应头并且自动追加charset。同时把格式信息透出在响应头字段X-Content-Source中方便调试时快速确认当前返回的是“原始 Markdown”还是“由 Markdown 渲染的 HTML”。另外我还把 Markdown 转 HTML 的渲染函数单独拆了出来这使得后续扩展其他格式比如 PDF、JSON时不需要改动接口主逻辑。5.3 客户端验证curl、requests 与 AI 代理模拟服务端写好后先启动服务python app.py然后用 curl 测试普通浏览器场景curl -H Accept: text/html http://127.0.0.1:5000/articles/article_1预期响应头中 Content-Type 为text/html; charsetutf-8响应体是渲染后的 HTML。再测试 AI 代理场景curl -H Accept: text/markdown http://127.0.0.1:5000/articles/article_1预期响应头 Content-Type 为text/markdown; charsetutf-8响应体直接是 Markdown 原文。如果你不希望手动敲 curl也可以写一个 Python 脚本来模拟 AI 代理。这里给出client/agent_request.py# 文件路径markdown-agent-demo/client/agent_request.py import requests url http://127.0.0.1:5000/articles/article_1 headers { User-Agent: AI-Agent-Bot/1.0, Accept: text/markdown, text/plain;q0.8, text/html;q0.5, } resp requests.get(url, headersheaders, timeout10) print(状态码:, resp.status_code) print(Content-Type:, resp.headers.get(Content-Type)) print(内容来源:, resp.headers.get(X-Content-Source)) print(响应正文前 300 个字符:) print(resp.text[:300])运行python client/agent_request.py如果一切正常你会看到 Content-Type 是text/markdown响应正文直接以# 一文搞懂 Accept 标头与 AI 代理开头。AI 代理拿到这段文本后可以不做任何标签清洗直接放入大模型上下文。5.4 扩展FastAPI 版本实现思路如果你用的是 FastAPI核心逻辑一样只是获取请求头和返回响应体的写法略有不同。下面给出一段参考实现# 文件路径markdown-agent-demo/fastapi_app.py from fastapi import FastAPI, Request, Response from fastapi.responses import HTMLResponse, PlainTextResponse app FastAPI() app.get(/articles/{article_id}) async def article(article_id: str, request: Request): # 实际项目中这里从数据库或文件读取 Markdown 内容 md_text f# {article_id}\n\n这是从 FastAPI 返回的 Markdown 内容。\n accept request.headers.get(accept, ) if text/markdown in accept: return Response( contentmd_text, media_typetext/markdown; charsetutf-8, ) # 简单将 Markdown 换行替换成 br 只是为了演示效果 html_content md_text.replace(\n, br/) return HTMLResponse(contenthtml_content)这段代码只演示思路没有引入复杂的依赖。FastAPI 中media_type参数可以直接控制响应头使用起来非常直观。5.5 预期效果与验证方法我们来梳理一个完整的验证路径。先请求不带任何 Accept 头的 URLcurl http://127.0.0.1:5000/articles/article_1这时 Flask 的默认行为会让best_match返回text/html普通用户和搜索引擎爬虫都能正常看到渲染后的页面。再让 AI 代理发起带Accept: text/markdown的请求curl -H Accept: text/markdown http://127.0.0.1:5000/articles/article_1服务端返回 MarkdownAI 代理能把内容直接作为上下文使用。这套交互完全遵循 HTTP 标准没有引入新的协议不管客户端是 OpenAI 的 Agent、自研的爬虫还是一个简单的 requests 脚本都能正常工作。6. 常见问题与排查思路6.1 常见问题表在实际部署过程中比较常见的问题如下。问题现象常见原因解决思路明明发了 Accept: text/markdown返回的还是 JSON服务端逻辑没有判断 Markdown或接口设计固定在 JSON检查接口代码增加 Accept 判断浏览器访问时收到了 Markdown 纯文本浏览器默认 Accept 头没有声明 text/markdown却被服务端强制返回检查服务端 fallback 逻辑最好默认 HTML中文内容在终端显示乱码Content-Type 缺少 charsetutf-8在响应中显式设置 charsetAccept 头已经写进去了但best_match返回 None候选列表和 Accept 头里没有重叠类型增加一个 fallback 类型例如 text/plain代理获取到 Markdown 后图片不显示Markdown 中的图片是相对路径在生成 Markdown 时将相对路径补全为绝对地址CDN 缓存了 HTML后续请求拿不到 Markdown缓存键没有把 Accept 头纳入配置 Vary: Accept 响应头返回 Markdown 时没有设置Content-Disposition用户点击链接直接把内容下载下来了在响应头中设置Content-Disposition: inline6.2 典型排查步骤如果遇到“返回的格式不是我想要的”这类问题建议按下面顺序排查。第一步确认客户端真实发送的 Accept 头。很多人会在 curl 里写-H Accept: text/markdown但代码里可能又用 aiohttp、requests 或者浏览器自动覆盖了。可以用抓包工具或者临时在服务端打印request.headers.get(Accept)来确认。第二步确认服务端内容协商逻辑。检查best_match候选列表是否正确以及 fallback 是否合理。第三步确认响应头。用curl -I查看响应头中的 Content-Type 与 Vary。如果响应头没有Vary: Accept那么 CDN 或浏览器缓存可能会引发“同一地址不同客户端拿到错误版本”的问题。第四步检查是否有中间件改写响应。Nginx、网关、框架中间件都有可能修改响应头尤其是现代 Spring Cloud Gateway、APISIX 这类网关层可能会做统一格式转换。7. 最佳实践与工程建议7.1 内容协商策略建议如果一个 URL 同时服务浏览器和 AI 代理建议设置明确的协商顺序优先text/markdown其次text/html最后text/plain。这样既能照顾 AI 代理的效率又能保证普通用户访问体验。很多人会认为不管客户端要什么我都返回 JSON 就好。但对 AI 代理来说JSON 虽然结构化却往往比 Markdown 更啰嗦而且真实的文档内容用 JSON 表示并不自然。Markdown 的优势在于“既是纯文本又有结构”它是接近自然语言的结构化表达。在实际业务中如果你同时想通过?formatmarkdown参数强制指定格式建议采用“Accept 头优先查询参数覆盖”的策略。这样高级用户可以显式指定普通代理也能通过标准头部完成内容协商。7.2 响应头与缓存设计提供 Markdown 内容时比较推荐的响应头组合是Content-Type: text/markdown; charsetutf-8 Content-Disposition: inline Vary: Accept Cache-Control: public, max-age300其中Vary: Accept非常关键。如果你希望浏览器代理缓存 Markdown 版本、普通浏览器缓存 HTML 版本就必须告诉 CDN 和浏览器响应内容会随着 Accept 头的变化而变化。如果省略了 Vary很可能用户第一次请求拿到 HTML 后下一个 AI 代理请求也被缓存成 HTML导致协商失效。对于需要频繁更新的内容建议缓存时间不要设置太长或者使用 ETag 做条件请求。7.3 安全与认证边界给 AI 代理开放内容接口并不意味着要把所有内部数据都公开出来。如果内容本身需要登录才能访问服务端一定要在内容协商之前完成认证和授权否则只要 AI 代理带上 Accept: text/markdown就能绕过页面限制直接拿到结构化全文。另外有些防护系统会把 AI 代理当作爬虫封禁。如果你的目标就是让内容被 AI 代理正常消费建议在 robots.txt 中明确允许对应路径并在 User-Agent 识别逻辑中放行常见的 AI 代理标识。如果担心恶意抓取或内容滥用可以在网关层增加 API Key、频率限制、敏感信息过滤等手段。注意这里说到的“放行 AI 代理”是指你在技术上愿意把公开内容以结构化方式提供给对方而不是绕过任何访问控制。对于涉及版权、隐私和数据安全的内容一定要先走法务和规范流程。7.4 可观测性与日志当接口同时返回多种格式时建议在访问日志中记录实际响应内容格式。比如增加响应头X-Content-Source或者在结构化日志中写入字段content_type_servedmarkdown|html|plain|json。对内容提供方来说你可能会关心“现在有多少 AI 代理在访问我的内容”。有了这一层日志你可以统计不同 Agent 的接受偏好甚至可以针对性地为头部 AI 代理优化内容质量。7.5 多格式支持的回退方案不要为了一味追求 Markdown而把所有非 Markdown 请求都返回错误。建议设计回退链text/markdown - text/html - text/plain - application/json。如果客户端真的很古老什么都不说就给一个 HTML 页面。如果客户端明确只接受 JSON可以在最后一个环节返回结构化错误或内容元信息。这样设计的好处是兼容性高。你不用担心“只支持 Markdown 会让老客户端坏掉”因为协商机制本身就是可降级的。8. 总结与下一步学习路线这篇文章主要讲了一件事通过 HTTP 的 Accept 标头让 AI 代理能够从同一个 URL 拿到更适合大模型解析的 Markdown 内容。为了达成这个目标我们从 HTTP 内容协商原理讲到 Flask 和 FastAPI 的具体实现再到缓存、日志、安全等工程细节代码示例都能直接运行。下一步建议你从三个方向继续深入。第一如果你在工作中接入了大模型知识库可以尝试用本文方案把公司内部的在线文档接口改造成“支持 Markdown 响应”然后观察知识库切片和检索质量是否有提升。第二学习缓存相关头字段尤其是 Vary、ETag、Cache-Control 的配合使用。真正上线时缓存策略往往决定这套方案能不能扛住高并发。第三研究一下目前主流 AI 助手的代码实现很多开源 Agent 框架里都有内容提取模块。你可以尝试在本地把响应的 Markdown 接入它们的上下文准备流程亲手对比一下 Markdown 和 HTML 在 token 消耗上的差距。如果这套方案在你的项目里成功落地欢迎把踩坑经历和优化数据记录下来。技术文章的价值不在概念多深而在能否让下一个开发者少走弯路。