去年年底我把公司几个站点的 SEO 工作流梳理了一遍,发现大部分时间都耗在重复劳动上:批量改标题、补描述、聚类关键词、查内容是否重复、检查 Meta 是否缺失。这些都是模板化任务,本质上是“阅读理解 + 规则匹配 + 输出结构化文本”,完全能被语言模型替代。真正让我停下动手写这套工具的契机,是 DeepSeek 的 API 价格和工具调用能力。免费额度时代已经过了,但即使按付费价格算,跑完一个月 SEO 任务的花费也比人工低两三个数量级;更重要的是它原生支持 function calling,这意味着代理可以在一次会话里连续完成“分析 -> 决策 -> 调用工具 -> 拿到结果 -> 再分析”的闭环。
这套工具的完整形态我命名得很朴素:DeepSeek 免费 SEO 自动化工具。它包含三个核心模块:模型路由、技能文件、智能代理扩展。文章不会教你写营销废话,全是实打实的架构设计、代码片段、部署参数和坑位记录。适合已经在做 SEO、同时想用 AI 把日常效率翻倍的开发者,也适合准备搭建自己 AI 工作流的独立站长。
1. 为什么用 DeepSeek 做这套工具:一个偏门但合理的选型逻辑
做 AI 自动化,第一步不是写代码,而是选模型。
1.1 SEO 任务对模型的三点特殊要求
我做过对比测试:同一批 SEO 任务分别丢给 GPT、Claude 和 DeepSeek,最后发现 SEO 场景和通用编程场景对模型的需求不太一样,主要有三点:
- 上下文窗口要够长:一篇 5000 字的博客文章,加上站点的相关页面列表、竞品页面内容、SEO 规则说明,单次任务的输入可能就要 8000 到 15000 token 甚至更多。上下文太短,你只能拆成多段喂入,代理的“全局视野”就打折扣。
- 结构化输出要稳定:SEO 工具最烦的不是模型不给结果,而是结果格式五花八门。同一个“标题生成”任务,模型有时候给纯文本,有时候给 Markdown,有时候给 JSON。这让下游的批量写入逻辑非常痛苦。
- 成本要低到可以“试错”:SEO 自动化的特点是请求量大、单任务价值低。生成一个 meta description,价值可能只有几毛钱,如果模型调用成本比人工还高,那自动化就毫无意义。
DeepSeek 在这三点的平衡上是目前最舒服的。它为 API 调用成本极低,所以我在设计代理循环时可以大胆地让模型反复思考、多次调用工具,而不用每轮都抠 token。同时它的上下文支持到 128K,足够读完整篇文章再输出分析。
1.2 模型路由、技能文件、智能代理扩展分别解决什么问题
这三个词是整套工具的骨架,分别对应一个具体痛点:
模型路由解决的是“浪费”问题。不是所有任务都需要模型满负荷推理。生成一条 alt 文本和制定整站内容策略,对模型能力的要求完全不同。如果所有请求都塞给同一个模型,你会为低难度任务支付不必要的推理成本,还拖慢响应。路由器做的事情,是把不同的 SEO 任务按复杂度分发到合适的模型,达到成本与质量的平衡。
技能文件解决的是“混乱”问题。SEO 方法论通常散落在各类文档、Notion、Excel 表格里,真正执行时全靠人肉回忆。技能文件把这些方法论写成结构化的 Markdown 文件,每个文件就是一个可复用的“SEO 技能包”,模型在需要时从技能库中检索并加载,类似给代理装上可插拔的专业模块。
智能代理扩展解决的是“断链”问题。单个 API 请求即使返回了高质量分析,也只是一个孤立的文本结果,无法触发后续操作。代理扩展把模型从“回答问题”变成“执行任务”:模型生成动作,代理执行动作,执行结果再回传给模型,形成循环,直到整个 SEO 任务完成。
把这三个模块拼在一起,你得到的实际效果是:向代理下达“优化这几篇过时文章的标题和描述”的指令,代理会调用抓取工具读文章内容,请路由器分配适合的模型,从技能文件里加载标题优化方法论,生成新标题和描述,再调用内容管理系统接口把这些改动写入系统,最后汇总一份改动报告发给你。
2. 模型路由层:一次请求该交给哪个模型,不该拍脑袋
模型路由是整个系统的第一层,也是最容易被忽视的一层。很多人在搭建 AI 工作流时习惯写死一个模型,我的建议是趁早改掉这个习惯。
2.1 我的三级路由设计
我的路由规则不是简单地按“难 / 易”二分类,而是分为三级:轻量级、标准级、深度级。
轻量级任务的特征是:输入短、输出短、规则明确、需要快速响应。包括 URL 规范化、标题去重、Meta description 提取、图片 alt 文本生成、内链锚文本判断。这种任务不需要模型拥有很强的推理能力,只要理解规则、格式输出稳定即可。
标准级任务的特征是:输入长度中等、需要一定理解能力、输出通常是一段话或结构化文本。包括博客文章标题优化、关键词归类、页面摘要生成、内容片段审核、FAQ 生成。这部分任务是 SEO 工作流里最日常的,对模型能力要求中等,但需要保证输出质量。
深度级任务的特征是:需要综合大量信息后做判断,输出是策略性结论。包括整站内容聚类、竞品差距分析、内容集群规划、反链策略建议、搜索意图推断。这类任务必须让模型在一个较大的上下文里反复思考,适合让推理能力更强的模型慢慢“想”。
路由器模块的核心代码如下,本质上就是读取任务类型配置,然后根据配置表把请求分发到对应的模型:
ROUTE_CONFIG = { "light": {"model": "deepseek-chat", "max_tokens": 512}, "standard": {"model": "deepseek-chat", "max_tokens": 2048}, "deep": {"model": "deepseek-chat", "max_tokens": 4096}, } def route_task(task_name: str) -> dict: if task_name in ["extract_alt", "dedup_title", "meta_description"]: return ROUTE_CONFIG["light"] if task_name in ["title_optimize", "keyword_group", "summary"]: return ROUTE_CONFIG["standard"] if task_name in ["content_cluster", "gap_analysis", "intent_inference"]: return ROUTE_CONFIG["deep"] return ROUTE_CONFIG["standard"]2.2 路由判断不能完全依赖规则表
规则表只适合任务类型固定的场景。真实项目里经常出现“看起来是轻量级任务,但实际上输入内容很复杂”的情况。比如“提取页面标题”看起来很简单,但如果页面是 SPBCA 单页应用,HTML 里的 title 标签可能是一个通用模板,真正有区分度的信息在 body 的 JSON 数据里。这时候如果按规则表无脑走轻量级路由,拿到的结果往往不可用。
所以我会在路由层加上一个预处理判断:先检测输入内容的长度和特征,如果超过阈值,自动把任务升级为下一级。例如 meta description 任务,如果摘要部分超过 2000 字符,表明模型需要先提炼长文本,这时候轻量级模型处理起来容易丢失重点,应该升级为标准级。
def pre_filter(task_name: str, input_text: str) -> dict: route = route_task(task_name) if len(input_text) > 2000 and route["max_tokens"] < 2048: route = ROUTE_CONFIG["standard"] return route这个升级机制还解决了一个隐性成本问题:模型处理超出能力范围的任务时,往往需要反复重试,每次重试都消耗 token,最终成本比直接用高一级模型还贵。提前升级,反而省钱。
2.3 路由层为什么要独立成服务
我在最开始是把路由逻辑内嵌在每个调用函数里的,后来发现一旦任务数量超过 30 个,内嵌逻辑就成了蜘蛛网。每个函数都有一份自己的路由判断,出问题时要逐个排查。
我把路由层拆成了独立模块,暴露两个接口:get_route(task_name)获取路由配置,execute_with_route(task_name, payload)根据路由配置执行请求。这样所有任务的模型分派都走同一个逻辑,出现路由问题时只需要检查一个文件。新增任务类型时,也只需在ROUTE_CONFIG里加一行配置,不用改动任何业务函数。
提示:路由配置里建议加上
fallback_model字段。当主模型因为服务压力或限流报错时,自动切换到备用模型重试一次,可以降低任务失败率。我在实测中遇到过一次模型服务波动,因为配置了 fallback,整个过程用户无感知。
3. 技能文件:把 SEO 方法论变成可插拔的提示词资产
大多数人和我一开始犯的错一样:把 SEO 方法论直接写在系统提示词里,结果系统提示词越来越长,直到某天改了其中一个规则,其他所有任务都被影响。
技能文件的思路,是把每个 SEO 技能封装成一个独立的 Markdown 文件,配上固定格式的 frontmatter 元信息。代理需要哪个技能,就动态加载哪个文件,互相不影响。
3.1 技能文件的目录结构和 Schema
我建了一个skills/目录,每个技能一个文件:
skills/ title-optimizer.md meta-description-generator.md keyword-cluster.md content-audit.md internal-link-suggest.md alt-text-generator.md每个文件的头部是 YAML frontmatter 声明元信息,正文是详细的提示词指令。以下是一个标题优化技能文件的示例:
--- name: title-optimizer description: 生成符合 SEO 规范且不过度堆砌的页面标题,通常控制在 30 字以内。 version: 1.2.0 input: - page_content - target_keyword - current_title output: json temperature: 0.7 --- 你是资深 SEO 内容编辑,擅长撰写既吸引用户点击又符合搜索引擎规范的标题。 规则: 1. 标题长度保持在 15-30 个字符之间,移动端显示友好。 2. 核心关键词尽量靠前,但禁止机械堆砌。 3. 不要使用"最好""最佳"等无法验证的绝对化表述。 4. 保留品牌词的位置,统一放在标题末尾,用短横线分隔。 5. 如果当前标题中包含年份信息,判断是否需要保留。 输出 JSON: {"title": "生成的标题", "reason": "简短说明生成理由"}这个格式不是随便定的。frontmatter 里的input字段声明了该技能所需的变量名,代理在加载技能文件后会先从自己的上下文里找这些变量,找不到就向工作流上一层申请。output字段声明了期望的输出格式,解析器会按这个格式校验结果。
3.2 技能加载与组合机制
有了技能文件,下一步是让代理知道“什么时候该用哪个技能”。我在系统提示词里不写任何具体 SEO 方法,只告诉代理:
你是一个 SEO 工作流代理。系统已提供技能文件库。执行任务时,请先从技能库中选择与子任务匹配的技能文件,读取其内容后按其中的指令执行。
具体匹配逻辑由一层轻量级检索完成。当代理收到“这篇文章的标题需要优化”的任务时,会先调用list_skills()拿到所有技能文件名和摘要,再调用load_skill("title-optimizer")把对应文件内容注入当前会话。
这里有一个关键取舍:一次只注入一个技能文件,而不是把全部技能文件都塞进上下文。如果一次性加载所有技能,会导致两个问题。第一,上下文被大量无关内容占满,留给真正任务文本的空间变少,模型注意力被稀释;第二,不同技能文件的规则之间可能有冲突,模型会不知道该优先遵守哪个。按需加载,让模型每次只关注一个任务、一套规则,输出质量明显更稳定。
3.3 技能文件版本管理
技能文件本质上是文本,天然适合放进 Git 仓库。我推荐的做法是为每个技能文件单独建版本记录,修改后更新 frontmatter 里的version字段。当代理执行任务时,如果发现技能文件版本有更新,可以在输出报告里附带一个skill_version字段,方便你回溯某个任务的输出是基于哪版规则生成的。
实测中这个机制极大减少了“为什么这次生成的结果和上次差那么多”的疑问。因为你能直接定位到技能文件版本变化对输出的影响,而不是对着模型随机性干瞪眼。
4. 智能代理扩展:把单次模型调用变成可持续运行的执行闭环
模型路由和技能文件都只是准备阶段,真正让这套工具“自动化”起来的是智能代理层。
4.1 工具注册表与代理循环
代理的核心是一个运行时循环,和大多数 Agent 框架的设计一致:读取用户任务 -> 调度模型 -> 模型可能需要调用工具 -> 执行工具 -> 把工具结果回传给模型 -> 模型生成最终输出。问题在于,这个循环里的每一步都可能让模型“跑偏”,尤其是工具定义写得不够清晰时。
先看工具注册表的设计。我按照 SEO 工作流的常见操作,注册了五类工具:
fetch_page(url):抓取页面内容并解析正文,返回纯文本。search_keywords(keyword, locale):调用关键词查询接口,返回相关关键词列表及搜索量。audit_meta(urls):批量检查一批 URL 的 Meta 标题/描述是否缺失。update_meta(url, title, description):调用内容管理系统接口,写入新的标题和描述。list_skills()/load_skill(name):技能文件查询与加载。
工具名我刻意设计得短且语义明确。模型在生成 function call 时会把任务描述映射到工具名,如果工具名含糊,例如do_meta_thing(),模型经常选错。
代理循环的 Python 骨架如下:
def run_agent(system_prompt: str, user_task: str): messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_task}, ] while True: response = client.chat.completions.create( model="deepseek-chat", messages=messages, tools=TOOL_SCHEMAS, ) msg = response.choices[0].message if not msg.tool_calls: return msg.content messages.append(msg) for tool_call in msg.tool_calls: result = execute_tool(tool_call.function.name, tool_call.function.arguments) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": result, })4.2 两个让代理真正“智能”的细节
很多代理样例代码能跑通,但一旦面对真实任务就罢工。原因往往出在下面这些我踩过坑的细节上。
第一个细节是工具返回结果的格式必须自描述。模型看到的fetch_page返回结果不是人眼看到的干净文本,而是原始抓取数据。如果抓取结果里包含 HTML 标签、脚本片段、Cookie 弹窗信息,模型的注意力会被严重误导。我在工具内部做了一层清洗,返回给模型的内容固定包含三块:
url:抓取地址。content_length:文本字数。cleaned_text:去标签后的正文前 2000 字。
模型需要更长的正文时会继续调用fetch_page(url, offset=2000)分段拉取。这个设计让每次工具调用的结果体积可控,也避免了语境过长问题。
第二个细节是限制同一个工具被连续调用的次数。有一次我让代理分析一个站点的 80 个 URL,它陷入了一个奇怪循环,比如连续调用fetch_page十几次后仍然不生成报告,因为每抓取一个新页面,它又发现新页面值得抓取。我给循环增加了最大迭代次数(通常设为 15 轮),超出后强制生成基于已获取信息的结论,并在报告中标注“未覆盖全部 URL”。这个限制救了无数个不眠夜。
4.3 遇到 “messages tool calls need immediate results” 错误怎么办
这是我在本地跑代理时遇到最多、也是最困惑的一个报错。报错全文类似“本轮运行失败: deepseek messages tool calls need immediate results”。如果你也在网上查过,大概率是在把 DeepSeek 接入 Cursor 或 Codex 这类工具时遇到的。
这个错误的核心是:在一次请求中,会话里出现了带tool_calls的 assistant 消息,但后续没有紧跟着对应role: "tool"的消息,或者出现了其他消息插队。API 要求工具调用的结果必须立即跟在产生该调用的消息之后。
解决方式不复杂,关键在于消息顺序必须严格遵循这个模式:
1. user message 2. assistant message (含 tool_calls) 3. tool message (tool_call_id 对应上一步的操作) 4. assistant message (含 tool_calls 或最终输出) 5. tool message ...如果你在构造messages数组时,在assistant message (含 tool_calls)之后插入了普通 user 消息或 system 消息,就会触发这个错误。修复的代码我放在下面,重点是把普通消息的追加放到工具结果之后,而不是每次循环都往数组里追加新 user 消息:
messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_task}, ] max_iterations = 15 for _ in range(max_iterations): response = client.chat.completions.create( model="deepseek-chat", messages=messages, tools=TOOL_SCHEMAS, ) msg = response.choices[0].message messages.append(msg) if not msg.tool_calls: return msg.content for tool_call in msg.tool_calls: result = execute_tool(tool_call.function.name, tool_call.function.arguments) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": json.dumps(result, ensure_ascii=False), }) return "迭代次数超限,已使用当前汇总信息。"我发现报错还有一个来源是:同时把 DeepSeek 配置在一个本来就为其他模型写好的客户端 SDK 上,而这个 SDK 会自动把某个状态消息插入messages列表。排查时请先检查你是否引入了任何中间层封装,比如 OpenRouter 适配器、缓存中间件、日志中间件,它们可能会打乱消息顺序。
注意:避免在工具名或函数参数里出现中文标点和特殊符号。我遇到过因为工具参数 JSON 里包含一个未转义的中文引号,导致整个 function call 解析失败的情况。使用
json.dumps(..., ensure_ascii=False)能规避大多数编码问题。
5. 实测记录:一个真实站点跑通 120 个页面的 SEO 优化任务
理论说再多,不如看一组实测数据。我拿自己运营的一个小型博客站做了实验,站点共 120 个有效页面。跑的任务是:检查所有页面的标题和描述缺失情况,断链页面不参与优化,对存在问题的页面生成优化方案。
5.1 运行环境与路由配置
- API:DeepSeek 官方 API,模型使用
deepseek-chat。 - 队列:用本地文件队列逐个处理 URL,方便断点续跑。
- 路由规则:标题缺失检查走轻量级任务;标题优化生成走标准级;聚合同一主题下所有页面时走深度级。
- 技能文件:加载
meta-description-generator和title-optimizer,共用一套运行循环。
5.2 成本和耗时统计
120 个页面的完整处理结果如下:
| 任务环节 | 处理页面数 | 平均耗时 | 消耗 token |
|---|---|---|---|
| 全站 Meta 缺失检查 | 120 | 6 秒 | 800 |
| 标题优化生成 | 34 | 25 秒 | 2200 |
| 描述补充生成 | 41 | 30 秒 | 3500 |
| 内容相关性核验 | 120 | 12 秒 | 1500 |
整个流程跑完约 20 分钟。这里的关键点在于:大部分页面其实没有 Meta 问题,真正需要重新生成标题和描述的不到 40%,所以代理在“预检查”阶段的效率直接决定了总耗时。这就是为什么第 4 节强调要把预检查做成一次轻量级工具调用,而不是让模型逐页分析。
费用方面,DeepSeek 官方价格已经便宜到“几乎没有记录价值”的程度,但不同地区的接口渠道和免费额度政策经常变化。我建议你自己跑一次小批量任务确认实际扣费,基本流程绝不会超过你一顿早餐的钱。
5.3 几个实际产出的真实case
选两个有代表性的输出给大家看。第一个是原来被忽略的旧文章,原标题是“如何用 Python 爬取电商网站”,技能文件优化后建议标题改为“Python 爬虫入门:用 X 路抓取电商商品数据(附代码)”。这个改法把“X 路”和“附代码”放在标题里,更符合搜索用户对教程类内容的预期。
第二个是一篇产品页,原有描述缺失,代理基于技能文件生成了描述:“面向中小团队的客服系统,支持多渠道接入与自动化工单分配,免费版可容纳 3 名客服。”这条描述把核心受众、功能点和免费版福利都放进去了,长度也卡在前 160 字符内。
5.4 真实体验里的效率瓶颈
数据好看的背面是,实测暴露了两个瓶颈。第一,处理整站时串行请求太慢,120 个页面花了 20 分钟,如果换成一万页的站,串行会慢到无法接受。解决方向是批量并发调用。DeepSeek API 对并发数有限制,但我实测控制在 5 个并发时既不会触发限流,又能把整体时间压缩到原来的 25% 以下。第二,内容质量评估环节还是需要人工抽验,工具能保证“格式规范”“规则符合”,但“这篇标题是否真的比原标题更好”,机器判断不了。我的做法是让代理生成 3 个候选标题,用脚本自动比对关键词覆盖度与长度,再由人工从候选里挑最优。
6. 踩坑与调优:路由误判、上下文太长、工具超时
最后这部分是我最想分享的,整套系统搭起来之后,稳定性比功能重要得多。我总结了四个最头疼的问题和对应的解决思路。
6.1 路由层对“复杂长文本”的误判
前面提到过,规则表对任务难度的判断是静态的,但输入内容是动态的。某次处理一个 3000 字的长尾关键词聚合任务,它被路由规则归到了“深度级”,模型反复阅读文本后输出了一份十几页的聚合报告,但其实用户只需要一个简洁的 Excel 表格,多出来的内容全是噪音。
调优策略:在路由层增加对输出格式的显式声明。无论任务多复杂,都先按output字段期望的格式裁剪输出长度。深度级任务如果预期是表格,就在系统提示词里明确“输出为一个表格,不要额外的分析段落”。这让模型的输出长度从“随心所欲”变成“按预期生产”。
6.2 上下文长度超出代理会话限制
技能文件的加载会吃上下文,工具返回结果也会吃上下文。累加之后,长会话经常在第五六轮工具调用后碰到上下文超限。我的处理方式是把历史消息做摘要压缩:当messages的总 token 数超过 30000 时,把最早的工具结果和 assistant 消息替换成一条摘要消息,只保留关键信息。
if estimate_tokens(messages) > 30000: summary = summarize_history(messages[: len(messages) // 2]) messages = [sys_msg, {"role": "user", "content": f"历史摘要:\n{summary}"}] + messages[len(messages) // 2 :]这个方法虽然损失了一部分细节,但保证了大多数长会话能跑完。对 SEO 任务来说,旧的抓取结果通常只影响中间判断,不影响最终输出,所以摘要丢失的细节在可接受范围内。
6.3 工具调用超时的处理策略
在线抓取页面时经常遇到响应超时的站点,代理会在工具调用处卡死,整轮任务中断。我的做法是给execute_tool包一层带超时控制的函数,超时后返回“该 URL 抓取超时”,让模型知道这个页面不可用,继续处理下一个。
import signal, timeout @timeout(20) def fetch_page_safe(url): return requests.get(url, timeout=10).text[:2000]默认超时我给 20 秒,如果对方站点本身比较慢,可以放宽,但过长的等待会拖垮整个队列。这类任务通常是边缘页面,跳过不是大问题。
6.4 模型“自信”导致的错误工具调用
最后一个是模型的心理层面问题。当代理需要批量优化 40 个页面时,模型有时会批量生成 40 个update_meta调用,而它其实只抓取过其中 10 个页面的内容。换句话说,它开始“凭感觉”生成不存在的优化数据。
我必须强调:不管模型多聪明,任何写操作工具都必须绑定真实的读取数据。我的解决方案是在工具 schema 里增加必填参数source_fetched,要求模型在调用update_meta时必须附带该 URL 的抓取结果摘要或抓取时间戳。如果模型无法提供,就说明它对该页面没有真实了解,这个操作会被运行时拒绝。这个“强制数据溯源”的小设计,直接让我避免了很多次错误写入。
整套系统跑到现在,我最大的体会是:AI 自动化不是替代 SEO 人员,而是把所有规则性、重复性的环节尽可能地吞掉,让人的精力集中在判断和策略上。模型负责干活,你负责定规则。技能文件是规则沉淀,模型是你的执行团队,而模型是你的项目管理员。搞清楚各自的角色边界,这套工具才能在真实工作里长期稳定地帮你省钱省时间。