一句话摘要
在 Codex 上搭建自我改进的外呼系统
如何在 Codex 上构建一个自我改进的外呼系统 July 19, 2026 · 14分钟阅读 · 查看原文 ↗ 金融 AI 移动应用 自动化
今年早些时候,Andrej Karpathy(@karpathy)把一个 agent 对准了他自己的训练代码,让它跑了两天。它跑了 700 个实验,保留了 20 个跑赢基准的,让模型训练快了 11%。然后他说了一句相当有意思的话:任何你能低成本评估的指标,都可以交给一群 agent。
回复率就是一个你能低成本评估的指标。从那时起,我花了一些时间研究,当这个循环对准外呼时,它长什么样。
我的构建:
Codex 读取上周的结果,编辑外呼系统所依赖的评分和话术文件,跑一次测试,然后开一个 pull request。它带着证据和分数,提出对 playbook 的修改建议,然后等待一个人来批准。发送和合并都留在循环之外。
我已经把第一个循环搭建过几次了:感知市场、给客户打分、根据信号写作、检查消息、记录结果、从回复中学习。这篇文章讲的是第二个循环——那个去编辑第一个循环的循环。
这就是那个构建:GTM(go-to-market)作为一份受版本控制、会随着市场自我改进的代码。
仓库
先从文件夹说起。它的结构很重要,因为 Codex 只能改进它能读到、能编辑的东西。
codex-self-improving-outbound/
AGENTS.md
README.md
config/
scoring.yaml
plays.yaml
prompts/
improve_scoring.md
improve_prompt.md
pr_summary.md
memory/
outcomes.jsonl
evals/
fixtures.yaml
score.py
scripts/
append_outcome.py
run_codex_step.sh
propose_improvement.py
open_pr.sh
weekly_tune.sh
examples/
outcomes.sample.jsonl
weekly-pr.md
这个仓库是故意做得很朴素的。config/scoring.yaml 保存着决定哪些信号重要的规则。prompts/ 保存着写消息的话术。memory/outcomes.jsonl 保存着市场做了什么。evals/score.py 是那道闸门,判断一个提议的修改是否真的有用。AGENTS.md 是 Codex 在碰任何东西之前要先读的"法律"。
先离线跑第一版。没有 CRM,没有数据补全,没有投递系统。改进循环应该在触及任何真实的外呼机器之前,先在本地文件上证明自己。
第 1 步:先写下法律
在写评分文件、写 prompt 文件之前,先写 AGENTS.md。这个文件让 agent 保持有用、并处于约束之内。
# 自我改进外呼规则
你根据结果来改进一个外呼系统。
硬性规则:
- 永远不要发送消息。
- 永远不要抓取或补全真实人物的信息。
- 永远不要自我合并。
- 只编辑这个仓库里的文件。
- 一次只改一个概念。
- 对每一个提议的修改,都要引用 memory/outcomes.jsonl 里的结果。
- 在一个修改能变成 PR 之前,先改进 evals/score.py。
- 如果评估没有改进,就回退你的编辑并停止。
允许的编辑:
- config/scoring.yaml
- config/plays.yaml
- prompts/*.md
必需的输出:
- 修改的文件
- 每个修改的理由
- 修改前分数
- 修改后分数
- pull request 摘要
这条法律只有一份工作:收窄工作范围。没有它,Codex 会试图通过扩张范围来"帮忙"。它会加更多数据、碰更多文件、调用更多工具,或者自动化一个本该留在人工控制下的步骤。而这里的工作要小得多:读取结果,提出一个文件的修改,证明它有用,然后等待。
"好"长什么样。你可以在批准一个 PR 之前读一遍这条法律,就能确切知道 Codex 被允许做什么。
它在哪里崩坏。法律变成一份合规文件。如果 AGENTS.md 需要一个目录,那它就已经太大了。让它保持可操作。
第 2 步:把判断搬进配置
大多数外呼判断都活在某个人的脑子里。然后团队买来软件,指望软件去改进一个它根本看不到的决策。
把判断搬进一个文件里。
signals:
competitor_comparison:
weight: 8
reason: "买家正在对比替代方案"
implementation_page_visit:
weight: 6
reason: "买家在确认这个能不能被落地"
job_repost:
weight: 5
reason: "岗位仍空缺且紧急"
funding_event:
weight: 5
reason: "预算或授权可能发生了变化"
generic_download:
weight: 1
reason: "内容兴趣,弱购买意图"
thresholds:
draft: 6
human_review: 10
negative_signals:
student_research: -8
vendor_pitch: -6
competitor: -10
这个文件一开始就是一份可见的假设。如果"generic download"应该算作零分,团队可以指着确切的那一行去改。如果"implementation page visit"是一个比你想的更强的信号,Codex 可以提出那个 diff,并展示能证明它的那些结果行。
不要把这个逻辑埋进一个 Python 函数里。如果规则是可见的,团队就能审阅它、跟它争辩、改进它,而不用把一个销售判断变成一次工程重构。
"好"长什么样。这个文件小到可以被拿来争辩。五个信号是一个好的第一版。
它在哪里崩坏。评分文件变成一个杂物抽屉。二十个信号、六个阈值、以及给每个边缘情况配的例外规则,会让改进器过拟合。从窄处开始,让结果告诉你下一个旋钮该放在哪。
第 3 步:把结果写成记忆
最重要的文件是 memory/outcomes.jsonl。
每一次触达一行,在结果确定时写入:
{"date":"2026-07-01","account":"Northwind Finance","signal":"competitor_comparison","play":"migration_note","score":8,"outcome":"reply","reason":"索要了迁移说明"}
{"date":"2026-07-01","account":"Bluepeak Studio","signal":"generic_download","play":"resource_followup","score":1,"outcome":"no_reply","reason":"纯内容意图"}
{"date":"2026-07-02","account":"KiteOps","signal":"implementation_page_visit","play":"implementation_angle","score":6,"outcome":"reply","reason":"询问了落地时间表"}
{"date":"2026-07-02","account":"Atlas Recruiting","signal":"job_repost","play":"hiring_angle","score":5,"outcome":"bad_fit","reason":"学生研究请求"}
reason 字段才是全部重点。no_reply 几乎什么都没告诉你。"纯内容意图"会告诉下一轮运行,这个信号可能不值得写一封草稿。bad_fit 只有在 reason 解释了为什么的时候才有用。"询问了落地时间表"这种细节,才是能改变一个权重的东西。
先建校验器,再建改进器:
构建 scripts/append_outcome.py。
它接受:
- date(日期)
- account(客户)
- signal(信号)
- play(话术)
- score(分数)
- outcome(结果):reply | meeting | no_reply | bad_fit | bounced
- reason(理由)
它拒绝:
- 缺失字段
- 未知的结果类型
- 空的 reason
- 未来的日期
把有效的行追加到 memory/outcomes.jsonl。
打印出被追加的那一行。
复利就从这里开始。一个仪表盘只能告诉你某个 campaign 在掉量。一份干净的结果日志,能告诉 Codex 在下一次运行之前,该改哪个信号、哪个话术、哪句措辞。
"好"长什么样。一周之后,一个陌生人都能读懂这个文件,说出哪些信号带来了回复,哪些话术带来了不适合的对话,以及团队内部最喜欢、市场却无视的是哪一个。
它在哪里崩坏。团队在周五凭记忆回填结果。赢家活了下来,bad_fit 的理由变得模糊,系统从虚构中学习。结果一落地,就立刻写那一行。
第 4 步:建好评估闸门
在 Codex 编辑任何东西之前,它需要一个它无法搪塞过去的测试。
创建 evals/fixtures.yaml:
cases:
- account: Northwind Finance
signals: [competitor_comparison, implementation_page_visit]
expected: human_review
note: "一个客户身上两个强信号"
- account: Bluepeak Studio
signals: [generic_download]
expected: ignore
note: "纯内容意图"
- account: KiteOps
signals: [implementation_page_visit]
expected: draft
note: "落地意图应该越过 draft 阈值"
- account: Atlas Recruiting
signals: [job_repost, student_research]
expected: ignore
note: "bad-fit 标记抵消了信号"
然后创建 evals/score.py:
构建 evals/score.py。
读取 config/scoring.yaml 和 evals/fixtures.yaml。
对每一个 case:
1. 把每个信号的权重加总。
2. 加上负信号的惩罚。
3. 给这个客户定路由:
- score >= thresholds.human_review => human_review
- score >= thresholds.draft => draft
- 否则 => ignore
4. 把路由和 expected 对比。
打印每一个预测。
打印最终准确率,格式为 score=0.00 到 score=1.00。
只有当准确率是 1.00 时才以 0 退出。
第一道闸门应该小到能看懂,又锋利到能抓住一次真正的漏判。在我的第一次运行里,基线挂掉了一个 case:
Northwind Finance: predicted=human_review expected=human_review
Bluepeak Studio: predicted=ignore expected=ignore
KiteOps: predicted=ignore expected=draft
Atlas Recruiting: predicted=ignore expected=ignore
score=0.75
那很好。系统把"落地意图"排在了 draft 阈值之下,所以它忽略了一个 fixture 说值得发消息的客户。在测试里抓住这个,比在一个月漏掉一堆客户之后才抓要好。
"好"长什么样。一条命令给出一个数字,而且每一个失败的 case 都很好检查。
它在哪里崩坏。fixture 只放了那些显而易见的赢家。然后每一个鲁莽的修改都能通过。把难看的 case 放进闸门里:弱意图、不匹配、无回复、过期的信号,以及那些你希望系统当初跳过的客户。
第 5 步:让 Codex 提出一个评分修改
现在 Codex 可以编辑了。
创建 prompts/improve_scoring.md:
你在改进外呼评分系统。
读取:
- AGENTS.md
- config/scoring.yaml
- memory/outcomes.jsonl
- evals/fixtures.yaml
你的工作:
1. 找到一条应该修改的评分规则。
2. 理由必须引用 memory/outcomes.jsonl。
3. 只修改 config/scoring.yaml。
4. 运行 python3 evals/score.py。
5. 如果分数提高,保留这个修改。
6. 如果分数持平或下降,回退你的修改并停止。
输出:
- 确切修改的那一行
- 导致它修改的那些结果行
- 修改前分数
- 修改后分数
- 这个修改是否应该成为一个 PR
不要编辑 prompt。
不要新增信号。
不要碰投递。
通过仓库包装脚本来运行它:
scripts/run_codex_step.sh improve_scoring
我的改进器的第一版犯了一个有用的错误。它去追逐看起来最干净的回复信号。competitor_comparison 在这份微小的结果日志里有着最强的回复率,所以改进器想提高那个权重。评估停在了 0.75,所以这个修改被拒绝了。
这正是这道闸门存在的原因。一个更弱的系统会因为那个说法听起来合理就接受它。而这一套问了一个更好的问题:这个修改有没有修好那个已知的漏判?
第二轮找到了那个最小的、有帮助的编辑:
- implementation_page_visit: 4
+ implementation_page_visit: 6
评估通过了:
Northwind Finance: predicted=human_review expected=human_review
Bluepeak Studio: predicted=ignore expected=ignore
KiteOps: predicted=draft expected=draft
Atlas Recruiting: predicted=ignore expected=ignore
score=1.00
这就是这个循环开始变得有用的那一刻。它改了一条规则,带着一个理由,并且对着一个 fixture 证明了那个修改。
"好"长什么样。提议的 diff 是无聊的、可追溯的:改了一行,附带一个基于结果的理由,改进了一次评估。
它在哪里崩坏。Codex 一次改三个权重和两个 prompt。现在没人说得清是哪个修改帮了忙。把法律守严:每个提案一个概念。
第 6 步:单独改进 prompt 文件
评分只是系统的一半。消息模板也会腐坏。
一句上个月还管用的话,开始变得耳熟。一个在某类客户里能换来回复的问题,在另一类里被无视。一句内部听着犀利的话,被市场惩罚。把 prompt 改进当成一条独立的赛道,这样 Codex 就不会把评分和文案混在同一个 PR 里。
创建 config/plays.yaml:
plays:
migration_note:
prompt_file: prompts/plays/migration_note.md
use_when:
- competitor_comparison
banned_lines:
- "觉得这可能跟你有关系"
- "快速问一下"
implementation_angle:
prompt_file: prompts/plays/implementation_angle.md
use_when:
- implementation_page_visit
banned_lines:
- "了解下我们的解决方案"
- "很想聊聊"
然后创建 prompts/improve_prompt.md:
你在改进一个外呼话术(play)。
读取:
- AGENTS.md
- config/plays.yaml
- memory/outcomes.jsonl
- 所选 play 对应的 prompt 文件
挑一个至少有 10 条结果的 play。
找出:
- 出现在正向结果里的行或结构
- 出现在 no_reply 或 bad_fit 结果里的行或结构
- 任何应该被禁用的短语
对这个 play 的 prompt 做一处小修改。
规则:
- 不要改评分。
- 不要新建 play。
- 不要新增渠道。
- 引用结果行。
- 写下修改前后的指令。
然后,如果有文案评估就运行它。
如果没有文案评估,就把 PR 标为 review_required 再打开。
有些改进可以自动打分。另一些仍然需要品味。如果没有文案评估,Codex 可以提出那个 prompt 编辑,但它应该把 PR 标记为待审阅,而不是假装那个编辑已被证明。
"好"长什么样。Codex 说:"这句话出现在七条 no_reply 结果里,所以我把它加进了 banned_lines,"或者"正向回复在第一句就引用了落地细节,所以我收紧了话术,要求必须那样写。"
它在哪里崩坏。改进器因为一条消息得到了回复,就把整个语气重写一遍。prompt 编辑应该比你本能想做的更小。
第 7 步:把修改以 pull request 的形式交付
这是控制层。Codex 编辑文件、运行评估、写下 PR 摘要。一个人审阅并合并。
创建 prompts/pr_summary.md:
为这次外呼改进写一个 pull request 摘要。
包含:
1. 改了什么。
2. 为什么改,引用结果行。
3. 修改前分数。
4. 修改后分数。
5. 修改的文件。
6. 风险。
7. 人工审阅者应该检查什么。
保持简短。
不要声称这个修改已经上线。
创建 scripts/open_pr.sh:
#!/usr/bin/env bash
set -euo pipefail
branch="codex/weekly-tune-$(date +%Y-%m-%d)"
git checkout -b "$branch"
git add config prompts evals memory
git commit -m "Codex weekly outbound tune"
body="$(cat outputs/pr-summary.md)"
python3 scripts/create_pr.py \
"$branch" \
"Codex weekly outbound tune" \
"$body"
PR 读起来应该像一个队友写的:
已修改:
- 把 implementation_page_visit 从 4 提高到 6。
为什么:
- KiteOps 有落地意图,并且回复了落地时间。
- 之前的评分把这个客户路由到了 ignore。
之前:
- 评估分数 0.75
之后:
- 评估分数 1.00
审阅者检查:
- 确认落地意图足够具体。
- 让 generic downloads 保持低分。
- 只有当这符合实际销售判断时才合并。
这就是安全系统。Codex 做枯燥的活。操作者守住标准。
"好"长什么样。每周一个 PR,小 diff,清晰的理由,通过的评估。
它在哪里崩坏。有人因为觉得审阅是一种摩擦,就给了 Codex 合并的权限。那一分钟,就分隔了一个"在改进"的系统和"在漂移"的系统。
第 8 步:给它定一个节奏
不要在每一条回复之后都跑这个。那样一个系统会过拟合到某一个吵闹的客户身上。
让一周自然过去,让结果累积起来,然后再调。
创建 scripts/weekly_tune.sh:
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
python3 evals/score.py || true
scripts/run_codex_step.sh improve_scoring
python3 evals/score.py
scripts/run_codex_step.sh pr_summary > outputs/pr-summary.md
scripts/open_pr.sh
然后 cron:
0 8 * * MON cd ~/codex-self-improving-outbound && scripts/weekly_tune.sh
如果你用 GitHub Actions,保持同样的结构:
name: weekly-outbound-tune
on:
schedule:
- cron: "0 8 * * 1"
workflow_dispatch:
jobs:
tune:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- run: pip install -r requirements.txt
- run: python3 evals/score.py || true
- run: scripts/weekly_tune.sh
前两次调优手动跑。读每一个 diff。观察当样本很薄的时候,Codex 试图改什么。一旦提案变得无聊,就把它放进日程里。
"好"长什么样。一个每周 PR 如期出现,带着证据、diff 和评估结果。你合并、编辑,或者关闭它。
它在哪里崩坏。任务跑了,没人审阅,PR 越堆越多。一个自我改进的系统仍然有一项人类习惯:读 diff。
克隆即运行的版本
仓库应该开箱就带四条命令:
git clone <repo>
cd codex-self-improving-outbound
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python3 evals/score.py
python3 scripts/propose_improvement.py
预期的第一次运行:
score=0.75
changed config/scoring.yaml
implementation_page_visit: 4 -> 6
score=1.00
open PR for human review
这个离线 demo 证明了文件契约。Codex 那次运行证明了编辑循环。在那之后,把你自己的结果替换掉样例结果,给信号重命名,加上你的话术,并构建一个能反映"你希望系统当初路由得不一样的那些客户"的 fixture。
不要从接投递开始。先从证明改进循环开始。
完整版:max
这个仓库是手动层。它从文件、公开信号和你的 Codex 计划来运行。因为每一条规则都是暴露出来的,它教你理解那个结构。
yourmax.ai 是同一套系统,只是把接缝藏了起来。
你不再需要自己把一个个仓库拼起来,max 就是你直接使用的那个 agent。它侦测市场动向,决定谁值得联系、为什么是现在,起草跨邮件和 LinkedIn 的外呼供你批准,并从结果里持续改进。
这个仓库展示的是大多数团队从未搭建过的自调层:结果变成提议的规则修改,提议的规则修改经过一道闸门,而人工的合并决定什么会成为线上生效的东西。max 把同样的运营逻辑拿过来,作为一个托管系统来运行。
如果你想要完整的仓库,可以告诉我,我会发给你。
标签:# X # 金融 # AI # 移动应用 # 自动化 # 指南
相关文章:构建个人 AI Agent 的 100 条技巧与窍门
这 100 条里有 15 条花了我六周中的四周才学会。它们被拆出来放在最后,按构建顺序排列。如果你这一期别的都不读,也必须读这 15 条,并在你还没……
AI 自动化 金融
原文参考:https://maxed.wiki/posts/how-to-build-a-self-improving-outbound-system-on-codex/ (Maxed.wiki,本页为站内中文整理)