摘要:把 AI Agent 塞进 cron job 让它自己跑,听起来很酷——真正跑起来才知道,从"能跑"到"稳定跑"中间隔着一堆坑。本文复盘我在 WSL2 环境里用 Hermes Agent 跑每日自动发文、热点追踪、数据报表这条流水线时踩过的 7 个生产级问题,以及怎么修好的。


上个月我在笔记本上搭了一套东西:每天早上 8 点,一个 AI Agent 自动醒来,搜热点、写文章、配封面、发 CSDN,然后通知我。全自动,零人工干预。

听起来像是"数字员工"的 demo 对吧?

第一周跑了 3 天,崩了 6 次。

不是那种"报个错就停了"的崩——是静默挂了,cron 显示成功,实际上什么都没产出。我早上打开电脑看到空荡荡的文件夹,那种心情……

算了,直接说坑。


这个东西大概长什么样

先给个整体架构图,后面看每个坑的时候你能对应上位置。

flowchart TB
    A[Cron 定时触发] --> B[Agent 启动]
    B --> C[加载 Skill 上下文]
    C --> D[读取任务 Map]
    D --> E{工具调用循环}
    E --> F[web_search 搜资料]
    E --> G[write_file 写文章]
    E --> H[publish.py 发布]
    F --> I[提取页面内容]
    I --> E
    G --> J[自检清单]
    J --> K{通过?}
| K -->|| L[飞书通知] |
| K -->|| M[patch 修复] |
    M --> G
    H --> L
    L --> N[退出]

| E -->|工具返回异常| O[重试或降级] |
    O --> E

核心流程不复杂:cron 唤醒 Agent → Agent 按 Skill 里的指令一步步调工具 → 产出文件 → 发布。问题全出在"一步步"这个环节


坑 1:上下文窗口爆炸——Agent 把自己说死机了

第一版的设计很 naive:把所有 Skill 文档、历史记录、系统 prompt 全塞进上下文窗口。

一个典型的 cron 任务跑起来,上下文大概长这样:

  • SOUL.md(行为约束):~3000 tokens
  • 4 个 Skill 文档(auto-publish、human-like-writing、trending-hub、wechat-title):~8000 tokens
  • 任务描述 prompt:~2000 tokens
  • 搜索返回结果(10 条 snippet):~3000 tokens
  • 页面提取内容(2-3 篇):~15000 tokens

加起来 31000 tokens,已经接近 DeepSeek V3 的上下文上限边缘了。

然后 Agent 开始写文章。写着写着又去搜第二轮资料,又拉进来 5000 tokens。窗口满了,API 直接返回 context_length_exceeded

但最坑的不是报错——是在窗口快满但还没满的时候,Agent 的行为会变得很奇怪:忘记前面的指令、跳过步骤、写了一半就"总结"了。

我一度以为是模型能力问题。 后来加了上下文用量监控才发现——窗口快炸了,模型在"仓促收尾"。

修法

# 每次调 API 前检查上下文用量
import tiktoken

enc = tiktoken.get_encoding("cl100k_base")

def check_context_budget(messages, max_tokens=64000):
    total = sum(len(enc.encode(m["content"])) for m in messages if "content" in m)
    usage_pct = total / max_tokens * 100
    if usage_pct > 80:
        # 触发压缩:用 LLM 摘要替代原始搜索内容
        return "summarize"
    elif usage_pct > 90:
        # 直接裁剪最旧的非关键消息
        return "trim"
    return "ok"

关键数值:80% 就预警,90% 就动刀。等 95% 再处理?晚了,模型已经开始乱来了。

对了,还有一个隐性成本——长上下文会让 API 调用变慢。3 万 tokens 的请求比 1 万 tokens 的慢 2-3 倍,而 cron job 通常有超时限制(我这设了 600 秒)。上下文长了,超时风险蹭蹭涨。


坑 2:工具调用失败后,Agent 直接装死

这是最让我崩溃的一个 bug。

Agent 在调用 web_extract 提取一个页面时,对方服务器返回了 503。正常逻辑应该是报错、Agent 看到错误、换一个 URL 重试。

但实际情况是:工具返回了一个空字符串 ""

Agent 看到 "",理解为"这个页面没有内容",然后跳过了这篇文章,基于不完整的信息继续写作。

最后产出的文章缺了关键数据,我自己读的时候才发现的——那个数据段落明显是"编"的,因为 Agent 找不到真实数据就自己脑补了。

工具协议缺少"错误语义"

回顾一下我当时写的工具封装:

def web_extract(url):
    try:
        resp = requests.get(url, timeout=30)
        return resp.text
    except Exception:
        return ""  # <-- 这里就是万恶之源

返回空字符串,Agent 分不清是"页面确实空白"还是"请求失败了"。

修正很简单——给错误一个明确的语义标记:

def web_extract(url):
    try:
        resp = requests.get(url, timeout=30)
        resp.raise_for_status()
        return resp.text
    except requests.Timeout:
        return "[ERROR: 请求超时,目标服务器无响应]"
    except requests.HTTPError as e:
        return f"[ERROR: HTTP {e.response.status_code}]"
    except Exception as e:
        return f"[ERROR: {str(e)}]"

加上 [ERROR: 前缀之后,Agent 的行为完全变了——它会识别这是失败,然后走重试或降级逻辑。模型其实不傻,是你给它的信息太模糊了。


坑 3:文件系统状态污染——昨天的垃圾影响今天的产出

cron job 每次都在同一个工作目录 /home/hnzwx/.hermes/scripts/articles/ 下跑。

第一天:write_file("article.md", content) → 成功。

第二天:Agent 先 search_files("*") 查看目录里有啥,然后基于已有文件做判断。结果它看到了昨天的 article.md,误以为"今天已经写过这篇文章了",直接跳过了写作步骤。

Agent 对文件系统有记忆,但它不知道哪些是"今天的"、哪些是"历史的"。

修正

每次 cron 启动时,先清状态:

# cron 脚本开头
WORKDIR="/home/hnzwx/.hermes/scripts/articles"
mkdir -p "$WORKDIR"/archive

# 把昨天的文件归档
find "$WORKDIR" -name "*.md" -mtime +0 -exec mv {} "$WORKDIR"/archive/ \;

或者更粗暴的——给每次运行一个独立的工作目录,跑完就清:

import tempfile, os
workdir = tempfile.mkdtemp(prefix="hermes_cron_")
os.environ["WORKDIR"] = workdir
# ... run agent ...
shutil.rmtree(workdir)

我目前在用第二种。隔离比清理靠谱。


坑 4:API 限流导致静默丢失任务

DeepSeek 的 API 有每分钟请求次数限制(RPM)。一个完整的发文流程大概需要 15-25 次 API 调用:

环节 调用次数 说明
热点搜索 2-4 次 搜索 + 页面提取
资料研究 3-5 次 深度搜索、多源验证
文章写作 8-12 次 分章节生成 + 自检修改
发布准备 2-3 次 标题优化、封面生成
通知 1 次 飞书消息

如果 RPM 限制是 30,理论上够用——但问题出在突发集中

Agent 在写文章阶段会连续调用 8-12 次,可能全部发生在 30 秒内。第 16 次调用触发限流,API 返回 429。

而我早期的代码里,429 的处理是:

except RateLimitError:
    return "[请求过于频繁,请稍后重试]"

Agent 看到这条消息之后……停下了。它理解为"系统在让它等",但它不会自己重试。任务就这样丢了。

修法

在 Agent 框架层做自动重试,对 Agent 透明:

from tenacity import retry, wait_exponential, stop_after_attempt

@retry(
    wait=wait_exponential(multiplier=1, min=2, max=60),
    stop=stop_after_attempt(5),
    retry=lambda e: isinstance(e, RateLimitError)
)
def call_llm(messages):
    return client.chat.completions.create(
        model="deepseek-chat",
        messages=messages
    )

Agent 感知不到限流,对它来说就是"调用稍微慢了一点"。这才是正确的抽象层次。


坑 5:时间感知错乱

这个问题很微妙。

Agent 的系统 prompt 里没有"当前时间"信息。它基于训练数据里的时间概念来理解"今天""昨天""本周"。

有一次我周日跑的 cron job,写的是周六的热点。Agent 在文章里写:"本周 AI 圈最火的事件是……"——但它说的"本周"其实是对应训练数据里的某个时间段,根本不是真实的"本周"。

文章发出去之后,评论区第一条:"你管这叫本周?这都上周的事了。"

尴尬。

修正

在 system prompt 里注入实时时间上下文:

from datetime import datetime, timezone, timedelta

beijing_tz = timezone(timedelta(hours=8))
now = datetime.now(beijing_tz)

time_context = f"""
当前时间: {now.strftime('%Y年%m月%d日 %H:%M')}(北京时间,UTC+8)
今天是星期{['一','二','三','四','五','六','日'][now.weekday()]}
"""
system_prompt = time_context + original_prompt

就这么几行代码,Agent 再也不搞错时间了。

但其实还有一个更难修的——搜索结果的时效性web_search 返回的结果可能是一周前的,Agent 不知道。你需要在 prompt 里明确告诉它:"搜索结果可能不是最新的,请根据返回内容里的日期标注来判断时效"。


坑 6:输出格式漂移

Agent 连续跑了一周之后,我发现文章格式在悄悄变化。

第一天:标准的 ## 标题- 列表```python 代码块。

第三天:标题变成了 ### 标题(多了一层)。

第五天:代码块的语言标记从 python 变成了 py

第七天:列表项的缩进乱七八糟,有些 2 空格、有些 4 空格。

这就是"格式漂移"——Agent 在多次运行中没有严格遵守格式约束,而是逐渐"自我调优"到了更松散的格式。

原因很简单:format instructions 和实际内容混在一起,经过多轮对话之后,instruction 的权重被稀释了。

修法

强制格式校验 + 自动修正:

import re

def validate_format(content):
    issues = []

    # 检查标题层级
    if content.startswith("# "):
        issues.append("禁止正文开头写一级标题")

    # 检查代码块语言标记
    code_blocks = re.findall(r'```(\w*)', content)
    for lang in code_blocks:
        if lang == "":
            issues.append("代码块缺少语言标记")
        elif lang == "py":
            issues.append("请使用 'python' 而非 'py'")

    # 表格首尾竖线检查
    table_lines = [l for l in content.split('\n') if l.startswith('|')]
    for line in table_lines:
        if not line.endswith('|'):
            issues.append(f"表格行缺少结尾竖线: {line[:30]}...")

    return issues

# 在写文件前校验
issues = validate_format(article_content)
if issues:
    # 把问题反馈给 Agent,让它自己修
    fix_prompt = f"请修复以下格式问题:\n" + "\n".join(f"- {i}" for i in issues)
    article_content = call_agent_fix(fix_prompt, article_content)

自动化格式检查 + Agent 自我修正 = 格式漂移被消灭。 不需要人去盯格式。


坑 7:费用静默失控

最后这个坑跟技术关系不大,但可能是最贵的。

Agent 跑起来之后,我设了每天发文,然后就忘了。一星期后打开 DeepSeek 的用量统计:

日期 API 调用次数 费用(估算)
6月20日 18 次 ¥0.52
6月21日 22 次 ¥0.68
6月22日 45 次 ¥1.42
6月23日 87 次 ¥3.01
6月24日 156 次 ¥5.83

6 月 23 日发生了什么?那天 Agent 在写文章时,搜索工具返回了一个循环引用——页面 A 引用页面 B,页面 B 又引用回页面 A。Agent 就一直在两个页面之间来回跳,每次调用都消耗 tokens,但我完全不知道。

一周跑掉了 ¥12。月均 50 块,够买两杯奶茶了——但问题不是钱,是你对系统的消耗完全失控。

修法

三层防线:

# 1. 工具调用次数上限
MAX_TOOL_CALLS = 30  # 任何任务不能超过这个值

# 2. 去重检查
visited_urls = set()
def web_extract(url):
    if url in visited_urls:
        return "[跳过:此 URL 已访问过]"
    visited_urls.add(url)
    # ... fetch

# 3. 费用预估
def estimate_cost(token_count, model="deepseek-chat"):
    rates = {"deepseek-chat": 2/1_000_000}  # ¥2 per 1M tokens
    return token_count * rates[model]

加上每日用量告警——超过 ¥2 自动飞书通知我。从那以后,费用曲线稳定得像条直线。


总结

回头看这 7 个坑,其实可以归纳成三类问题:

问题类型 具体表现 核心对策
上下文管理 窗口爆炸、指令稀释、格式漂移 预算监控 + 主动压缩 + 格式校验闭环
错误处理 工具失败静默、限流丢任务、循环引用 错误语义化 + 框架层自动重试 + 去重
环境治理 状态污染、时间错乱、费用失控 隔离工作目录 + 时间注入 + 用量告警

说白了就是一句话:AI Agent 从 Demo 到 Production,中间差了一个"运维"的活。

模型本身没问题,API 也没问题。是你没把它当正经的生产系统来管——没有监控、没有限流、没有校验、没有隔离。任何一个后端服务上线都要这些东西,Agent 也不例外。


你也在跑 Agent 的 cron job 吗?踩过什么离谱的坑?评论区说说,我收集起来更新到这篇文章里。

🛑 质检员合规自检表

检查项 状态
正文开头无 # 标题
所有代码块有语言标记
代码块前后有空行
表格格式正确(竖线完整)
标题层级连续不跳级
CSDN 审核红线:代理/proxy/VPN ✅ 无
CSDN 审核红线:curl|bash/irm|iex ✅ 无
CSDN 审核红线:白嫖 ✅ 无
Mermaid 图 ✅ 1 张
对比表格 ✅ 2 张
可运行代码 ✅ 多段
开放式问题结尾
Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐