这次我们来看一个和 AI 代理(Agent)协作非常相关的小技巧:用Accept标头让服务端直接返回 Markdown 内容。
很多做 Agent 工具链、知识库抓取、网页内容提取的开发者,应该都遇到过同一个痛点:抓回来的内容是一堆 HTML 标签和脚本噪声,喂给大模型后 token 消耗大、解析容易出错。传统做法是抓完再清洗、再转换,中间还要维护一堆解析规则。但如果服务端本身支持内容协商,客户端只需要在请求里加一个Accept: text/markdown,就能直接拿到结构干净的 Markdown。
这篇文章不聊概念,直接讲清楚四件事:Accept标头在 AI 代理场景里到底怎么用;服务端怎么实现按标头返回 Markdown;客户端和批量任务怎么写;踩坑之后怎么排查。如果你正在做 AI 代理、RAG 知识库、网页摘要工具,或者想把 Markdown 内容接入自己的工作流,这篇文章可以直接收藏。
1. 核心能力速览
先说结论:这个方案的核心不是某个具体软件,而是 HTTP 内容协商机制在 AI 代理链路上的应用。
| 能力项 | 说明 |
|---|---|
| 主题类型 | HTTP 内容协商 / AI Agent 集成 / 文档获取 |
| 核心机制 | 请求标头Accept: text/markdown,服务端按客户端偏好返回 Markdown |
| 主要功能 | 让 AI 代理直接获取结构化 Markdown,替代 HTML 清洗后转换 |
| 适合场景 | AI 代理网页工具、RAG 知识库、网页摘要、批量内容抓取、Markdown 渲染链路 |
| 涉及技术 | HTTP、HTTPX/requests、FastAPI/Flask、SSE 流式输出、Markdown 解析渲染 |
| 支持 API | 取决于服务端实现;客户端侧只要支持自定义请求标头即可 |
| 是否支持批量 | 支持,按请求级别处理,可并发批量抓取 |
| 显存需求 | 不涉及 |
| 启动方式 | 服务端按业务框架启动;客户端代码调用;也可在 API 网关/代理层实现 |
| 使用门槛 | 需要了解 HTTP 标头语义,能改请求标头,能读服务端响应 |
从实际价值看,这个方案最值得关注的不是协议本身,而是它能减少 AI 代理链路里的“内容清洗”环节。只要服务端或者中间代理层支持,客户端一次请求就能获得结构化 Markdown,后续无论是存知识库、做摘要还是转其他格式,都省一步。
2. 适用场景与使用边界
2.1 这个方案适合谁
第一类是 AI 代理开发。Agent 调用网页工具时,拿到 Markdown 比拿到 HTML 更容易让模型理解。尤其是有代码块、表格、链接列表的页面,Markdown 能保持结构,语义损耗更低。
第二类是 RAG 知识库构建。知识库入库前最耗时的一步是清洗 HTML。如果数据源支持 Accept 内容协商,抓取阶段直接得到 Markdown,清洗成本会大幅下降。
第三类是内容工具链开发。很多 Markdown 编辑器、文档转换工具(参考常见的 Markdown 转 Word 工作流、Markdown 渲染 HTML、Markdown 表格复制等场景)都依赖标准 Markdown 作为中间格式。用 Accept 标头获取内容,可以统一输入格式。
2.2 不适合什么场景
这套方案不适合必须保留原始 DOM 结构的场景。有些前端页面依赖 JavaScript 渲染,服务端返回的 Markdown 是降级内容,缺失交互信息。此时强行用Accept: text/markdown反而丢了数据。
也不适合所有服务端已经写死返回 HTML 的场景。如果服务端没有实现内容协商,发什么 Accept 都不会改变响应体格式,这时候只能靠解析层兜底。
2.3 使用边界与合规提醒
使用网页内容时,要注意版权和授权边界。爬取内容后用于 AI 训练、商用产品、内容再发布,都需要确认数据源的授权情况。不要把未经授权的文章整篇入库后对外输出。涉及需要登录、付费内容、个人隐私数据时,更要注意合规,只抓取公开且允许访问的数据,并遵守目标站点的 robots 协议和服务条款。
3. 环境准备与前置条件
这个方案不挑硬件,普通开发机就可以。需要准备的是运行环境和依赖库。
| 项目 | 说明 |
|---|---|
| 操作系统 | Windows / Linux / macOS 均可 |
| Python | 建议 3.9 以上 |
| 依赖库 | FastAPI、uvicorn、requests、httpx |
| 测试工具 | curl 或 Postman |
| 网络环境 | 能访问目标服务端即可 |
安装依赖:
pip install fastapi uvicorn requests httpx如果没有特殊要求,不需要 GPU、不需要 Docker、不需要额外模型文件。整个方案是 HTTP 层面的,和本地算力无关。
4. 服务端实现:按 Accept 标头返回 Markdown
4.1 FastAPI 内容协商示例
服务端的关键逻辑是:读取请求的Accept标头,如果包含text/markdown,就返回 Markdown;否则返回 HTML 或纯文本。
from fastapi import FastAPI, Request from fastapi.responses import HTMLResponse, PlainTextResponse from fastapi.responses import Response app = FastAPI() MARKDOWN_CONTENT = """# 项目说明 这是一个支持 **Accept: text/markdown** 的示例服务。 ## 功能列表 - 内容协商 - Markdown 返回 - AI 代理友好 | 格式 | 说明 | | --- | --- | | HTML | 完整页面 | | Markdown | 结构化文本 | """ HTML_CONTENT = """<!DOCTYPE html> <html> <head><title>项目说明</title></head> <body> <h1>项目说明</h1> <p>这是一个支持 <strong>Accept: text/markdown</strong> 的示例服务。</p> <ul> <li>内容协商</li> <li>Markdown 返回</li> <li>AI 代理友好</li> </ul> </body> </html> """ @app.get("/docs") async def get_docs(request: Request): accept_header = request.headers.get("accept", "") if "text/markdown" in accept_header: return Response(content=MARKDOWN_CONTENT, media_type="text/markdown") return HTMLResponse(content=HTML_CONTENT)启动服务:
uvicorn main:app --host 0.0.0.0 --port 80004.2 代理层转换方案
如果你的数据源是第三方站点,服务端不能改,你可以在自己的代理服务里做转换。收到客户端的Accept: text/markdown后,代理层去抓取目标 HTML,再通过 html2text 等工具转成 Markdown 返回给客户端。
import html2text import requests from fastapi import FastAPI, Request from fastapi.responses import Response app = FastAPI() @app.get("/proxy") async def proxy(request: Request, url: str): accept_header = request.headers.get("accept", "") resp = requests.get(url, timeout=15) resp.encoding = resp.apparent_encoding if "text/markdown" in accept_header: converter = html2text.HTML2Text() converter.ignore_links = False markdown_content = converter.handle(resp.text) return Response(content=markdown_content, media_type="text/markdown") return Response(content=resp.text, media_type="text/html")这种代理层方案是目前 AI 代理工具里比较通用的做法:上游数据源格式不可控,但下游统一输出 Markdown,Agent 拿到的内容始终是干净的。
5. 客户端请求与效果验证
5.1 curl 请求验证
先验证服务端是否支持内容协商。
不带Accept标头,默认返回 HTML:
curl -s http://127.0.0.1:8000/docs带Accept: text/markdown,返回 Markdown:
curl -s -H "Accept: text/markdown" http://127.0.0.1:8000/docs判断成功的标准:第一段命令返回 HTML 页面;第二段命令返回带#标题、-列表、|表格的 Markdown 文本。如果两段命令返回结果一样,说明服务端没有做内容协商。
5.2 Python 客户端调用
import requests url = "http://127.0.0.1:8000/docs" headers = { "Accept": "text/markdown", } response = requests.get(url, headers=headers, timeout=15) print(response.status_code) print(response.headers.get("content-type")) print(response.text)如果控制台输出包含# 项目说明、功能列表等 Markdown 结构,说明请求链路正常。如果输出是 HTML 标签,说明服务端忽略了Accept标头。
5.3 SSE 流式输出场景
AI 代理场景经常用 SSE(Server-Sent Events)做流式输出。Markdown 内容在流式输出时要特别注意:部分 Markdown 语法是跨片段组合的,比如表格的|分隔行、代码块的 ``` 围栏、列表的多行结构。如果 SSE 直接把文本按片段抛给前端,渲染器可能会在中间状态出现闪烁。
解决办法是前端配合 Markdown 渲染器做“合并渲染”:只对累积后的完整内容渲染,或者用支持流式渲染的 Markdown 组件。很多开发者检索 Markdown 渲染器、SSE 流式输出 Markdown 渲染器,核心都是要处理这种“边输出边渲染”的体验问题。
6. 接口 API 与批量任务
6.1 批量抓取 Markdown 内容
实际业务中,Agent 往往要一次处理多个 URL。批量任务的核心是:每个请求都携带Accept: text/markdown,并做好并发控制和错误重试。
import asyncio import httpx URLS = [ "http://127.0.0.1:8000/docs", "http://127.0.0.1:8000/docs", "http://127.0.0.1:8000/docs", ] async def fetch_markdown(client, url): headers = {"Accept": "text/markdown"} try: response = await client.get(url, headers=headers, timeout=20) response.raise_for_status() return { "url": url, "status": response.status_code, "content_type": response.headers.get("content-type"), "content": response.text, } except Exception as exc: return { "url": url, "status": "failed", "error": str(exc), } async def main(): async with httpx.AsyncClient() as client: tasks = [fetch_markdown(client, url) for url in URLS] results = await asyncio.gather(*tasks) for result in results: if result["status"] == 200: print(f"成功: {result['url']} -> {result['content_type']}") print(result["content"][:200]) else: print(f"失败: {result['url']} -> {result.get('error')}") if __name__ == "__main__": asyncio.run(main())6.2 批量工程的几个建议
批量任务的稳定性比单次请求更重要。常见问题是目标站点限流、超时、编码混乱、上游偶发 500。建议按这个顺序处理:
- 控制并发数,不宜过高,给目标服务留出余量。
- 添加重试逻辑,对 429、503 等状态做指数退避。
- 记录每个 URL 的抓取日志,方便失败后定点排查。
- 输出结果落盘保存,避免进程中断后全部重跑。
- 如果目标是同一个站点,要控制请求频率,避免对目标服务造成压力。
7. 资源占用与性能观察
这个方案不消耗 GPU,性能瓶颈主要在网络请求、HTML 转换、Markdown 渲染这几个环节。
观察点有三个。
7.1 服务端响应体积
HTML 页面通常包含大量标签、脚本、样式,返回体积可能几十 KB 甚至更大。Markdown 内容通常只有几 KB 到十几 KB。对 AI 代理来说,体积越小,token 消耗越低,模型理解越直接。
7.2 转换耗时
如果使用代理层做 HTML 转 Markdown,转换耗时会随页面复杂度增加。大表格、多层嵌套标签、图片链接较多的页面,转换时间会更长。建议在代理服务里加缓存:同一个 URL 的 Markdown 结果缓存一段时间,避免重复抓取和重复转换。
7.3 并发连接数
批量任务下,连接数直接取决于并发设置。HTTPX 的AsyncClient默认连接池是有限制的,建议显式配置,避免连接耗尽。
async with httpx.AsyncClient(limits=httpx.Limits(max_connections=20, max_keepalive_connections=10)) as client: ...如果发现大量请求 socket 超时,优先检查是否触发了目标服务限流,而不是无脑调高并发。
8. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 服务端返回 HTML,不是 Markdown | 服务端没有实现内容协商逻辑 | curl 带 Accept 标头看响应头和响应体 | 在代理层做 HTML 转 Markdown |
| 修改 Accept 标头后响应没变 | 请求标头没传到服务端或被网关剥离 | 服务端打印 request.headers 检查 | 检查网关、反向代理的标头透传配置 |
| Markdown 内容里中文乱码 | 上游响应编码识别错误 | 打印响应源码检查 meta charset | 使用resp.apparent_encoding或指定 UTF-8 解码 |
| 批量任务中途大量超时 | 并发过高触发限流 | 查看目标服务返回状态码 | 降低并发,增加重试退避 |
| 返回的 Markdown 里表格解析不全 | 上游 HTML 表格结构不规范 | 检查原始 HTML 的 table 结构 | 在转换前预处理,或改用更强解析库 |
| 流式输出时 Markdown 渲染闪烁 | SSE 片段和 Markdown 语法组合冲突 | 观察累积文本渲染效果 | 合并累积内容后渲染,使用流式 Markdown 渲染器 |
| URL 带中文参数请求失败 | 未做 URL 编码 | 检查请求日志 | 使用urllib.parse.quote或 requests 自动编码 |
8.1 Accept 标头被忽略怎么处理
这是最常遇到的问题。很多服务端框架默认返回 HTML 或 JSON,不解析Accept标头。不要指望所有站点都支持内容协商。
处理方式有两种。
第一种是服务端自己实现,参考第 4.1 节的 FastAPI 示例,在路由里读取请求标头并分支返回。
第二种是加到自己的 AI 代理层。代理层保持对下游的兼容性,对上游可以是任意格式,对客户端统一输出 Markdown。这样即使上游是 HTML,Agent 拿到的依然是 Markdown。
8.2 缓存导致旧内容
如果服务端或代理层加了缓存,修改 Markdown 内容后客户端可能拿到旧版本。排查时可以给请求加一个随机的查询参数打散缓存:
curl -H "Accept: text/markdown" "http://127.0.0.1:8000/docs?ts=1234567890"也可以用Cache-Control: no-cache标头:
curl -H "Accept: text/markdown" -H "Cache-Control: no-cache" http://127.0.0.1:8000/docs但要注意,目标服务端不一定遵守这个标头。最稳妥的做法还是在代理层明确控制缓存策略。
9. 最佳实践与使用建议
9.1 客户端默认带上 Accept 标头
做 AI 代理时,可以把Accept: text/markdown设为默认请求头。如果目标服务端不支持内容协商,结果就是返回原内容,不会有额外损失;如果支持,就拿到了更干净的输入。
9.2 不要让 Accept 标头承担鉴权职责
Accept标头只表达内容偏好,不能用于身份验证和权限控制。不要把“允许 Markdown 访问”当作“已授权内容获取”的依据。敏感内容的访问控制要依赖认证授权机制,而不是内容协商。
9.3 把 Markdown 结果缓存到本地
对 AI 代理链路来说,重复抓取同一个 URL 的成本很高。建议在代理层维护一套 Markdown 缓存,缓存 key 可以是 URL 加 Accept 标头的组合。这样既减少目标服务压力,也减少网络等待时间。
9.4 与 Markdown 工具链结合
拿到标准 Markdown 后,生态非常丰富。可以接 Markdown 编辑器做人工审阅,可以用渲染器转成 HTML 预览,可以配合 Markdown 转 Word 工作流输出报告,也可以直接存入知识库做向量化。让整条链路围绕 Markdown 展开,可以避免“每个环节维护一套格式”的重复开发。
9.5 版权和数据合规
内容抓取只是第一步。抓取后如果还要喂给本地模型做摘要、做训练、做二次发布,必须确认数据来源是否允许。优先处理自己拥有授权、开源许可明确、或者公开允许抓取的数据。对来源不明的 HTML 页面,即使能通过Accept: text/markdown获取到内容,也不能默认获得商用授权。
9.6 保留一套最小验证流程
建议在项目里保留一个最小例子:一个 FastAPI 服务端、一个 curl 命令、一个 Python 客户端脚本。后续改造代理层、新增数据源、调批量逻辑时,先用这套最小流程验证内容协商是否正常,再接入复杂业务,能省很多定位问题的时间。
10. 总结与下一步
用Accept标头向 AI 代理提供 Markdown 内容,思路本身很简单:客户端声明偏好,服务端按偏好返回。但它对 AI 代理链路的优化是实打实的,省掉了 HTML 清洗、标签去除、格式转换这些最耗时的中间环节。
如果你现在正在做 AI 代理或者知识库抓取,建议先做两件事:一是给客户端请求加上Accept: text/markdown,确认数据源是否支持内容协商;二是在自己的代理层实现一个 HTML 转 Markdown 的兜底转换,保证下游始终拿到统一格式。
最容易踩的坑是三个:服务端根本没做内容协商,标头发了也白发;批量任务并发过高触发限流;HTML 转 Markdown 后表格和代码块结构丢失。这三个问题都可以用“代理层兜底 + 控制并发 + 转换后抽查”的方式解决。
把这套逻辑跑通之后,后面还可以继续扩展:接入 SSE 流式输出做增量渲染,把 Markdown 结果缓存成独立的知识库,或者把转换能力封装成内部 API 服务。内容协商只是第一步,关键是把 Markdown 变成后续所有 AI 代理任务的统一输入格式。