Codex 同线程续跑:AI 编程助手第一次真正"记住了你在干什么"
Codex 同线程续跑:AI 编程助手第一次真正"记住了你在干什么"
我让 Codex 帮我做第三步补测试的时候,它把第一步定义的函数名全写错了。
不是写错一个,是全错。test_create_user_with_valid_email 变成了 test_user_creation,mock_db_session 变成了 mock_session——它根本不知道第一步发生了什么,就像一个刚被叫进会议室、完全不知道前两小时讨论了什么的新同事,只能凭直觉瞎猜。
这不是 Codex 的 bug,这是旧机制的设计限制。而 Automations 同线程续跑功能,就是专门为了解决这件事而来的。
---
一、先把概念说人话
同线程续跑(Continue in Same Thread),用一句最直白的话说就是:Codex 的 Automations 任务现在可以在同一个上下文对话线程里接着跑,而不是每次重新开一个空白对话。
听起来像废话,但你想想之前是什么情况——
旧版 Automations 每次触发任务,都是一个全新的 context window。Codex 不知道上一步用了什么函数名、不知道你们约定了什么代码风格、不知道上一步的测试已经覆盖了哪些边界。它每次都是"刚入职的新人",你得从头解释。
新版的逻辑是:同一个 Automation 任务链条内,多个步骤共享同一个 thread。历史消息、代码状态、命名约定,都可以被"记住"。它现在更像一个真正跟你并肩作战的同事——知道上一步做了什么,知道为什么这样做。
本文不讲官方公告,只讲一个真实任务跑下来你能看到什么变化。
---
二、机制拆解:它到底改了什么
要真正理解这个功能,需要先搞清楚三个概念。
Thread 是什么
在 OpenAI 的 Assistants API 体系里,Thread(线程) 是一个持久化的对话容器。你可以往里面追加消息,模型每次回复都会基于这个 thread 里的完整历史来生成。
类比一下:Thread 就像一个 Google Doc,所有参与者都在同一个文档里协作,历史修改记录都在。旧机制是每次任务都新建一个 Doc,新机制是所有步骤都在同一个 Doc 里接着写。
Automation 任务的触发逻辑
Codex Automations 本质上是一套多步骤任务编排系统,你可以定义一个任务流:分析代码 → 生成测试 → 检查覆盖率 → 补充用例 → 输出报告。
旧版每一步都是独立的 API 调用,没有状态传递。新版通过 thread_id 把这些步骤串联起来,后续步骤可以"看到"前面步骤的完整对话历史。
续跑意味着什么
续跑不是"无限上下文",这是一个常见误区。
它的准确含义是:在同一个 Automation 任务链条内,上下文被复用。一旦超出这个任务范围——比如你开一个全新的 Automation、或者任务链条太长导致早期消息被压缩——依然会有 context 边界。
用一张流程图来对比:
旧机制:
任务A Step1 [Context: 空] → 任务A Step2 [Context: 空] → 任务A Step3 [Context: 空]
↑ 每步都不知道前面发生了什么
新机制:
任务A Step1 [Thread: T001] → 任务A Step2 [Thread: T001] → 任务A Step3 [Thread: T001]
↑ 能看到Step1的历史 ↑ 能看到Step1+Step2的历史
这个差异,在简单任务里感知不明显。但一旦任务超过三步、涉及多文件、有命名约定要求,差距就会被放大。
---
三、真实任务实测:从需求到交付
我选了一个典型的多步骤任务来验证这个机制:
给一个 Python Flask API 项目,自动生成单元测试 → 发现覆盖率不足 → 补充边界用例 → 输出测试报告摘要。任务起点:原始项目结构
项目是一个简单的用户管理 API,核心文件 user_service.py:
# user_service.py
from flask import Flask, jsonify
from models import User, db
app = Flask(__name__)
def create_user(email: str, username: str) -> dict:
"""创建新用户,返回用户信息字典"""
if not email or "@" not in email:
raise ValueError("无效的邮箱格式")
if db.session.query(User).filter_by(email=email).first():
raise ValueError("邮箱已被注册")
user = User(email=email, username=username)
db.session.add(user)
db.session.commit()
return {"id": user.id, "email": user.email, "username": user.username}
def get_user_by_id(user_id: int) -> dict:
"""根据 ID 查询用户"""
user = db.session.query(User).filter_by(id=user_id).first()
if not user:
raise ValueError(f"用户 {user_id} 不存在")
return {"id": user.id, "email": user.email, "username": user.username}
Step 1:生成初始单元测试
Codex 在 Step 1 生成了这个测试文件,并且在 prompt 里明确约定了命名规范:
# test_user_service.py(Step 1 生成)
import pytest
from unittest.mock import MagicMock, patch
from user_service import create_user, get_user_by_id
命名约定:test_{函数名}_{场景描述},mock 对象统一用 mock_db_session
@pytest.fixture
def mock_db_session():
"""统一的数据库 session mock"""
with patch('user_service.db.session') as mock_session:
yield mock_session
def test_create_user_with_valid_email(mock_db_session):
mock_db_session.query.return_value.filter_by.return_value.first.return_value = None
result = create_user("[email protected]", "testuser")
assert result["email"] == "[email protected]"
def test_get_user_by_id_existing_user(mock_db_session):
mock_user = MagicMock(id=1, email="[email protected]", username="testuser")
mock_db_session.query.return_value.filter_by.return_value.first.return_value = mock_user
result = get_user_by_id(1)
assert result["id"] == 1
Step 2:覆盖率检测
Codex 分析了 Step 1 的测试,发现覆盖率只有 62%,缺少以下场景:
create_user的邮箱格式校验分支create_user的重复邮箱分支get_user_by_id的用户不存在分支
Step 3:补充边界用例(关键验证点)
这一步是最能体现同线程续跑价值的地方。Codex 生成的补充测试:
# test_user_service.py(Step 3 补充,注意命名一致性)
def test_create_user_with_invalid_email(mock_db_session):
"""测试无效邮箱格式——复用 Step 1 定义的 mock_db_session fixture"""
with pytest.raises(ValueError, match="无效的邮箱格式"):
create_user("not-an-email", "testuser")
def test_create_user_with_duplicate_email(mock_db_session):
"""测试重复邮箱注册"""
existing_user = MagicMock()
mock_db_session.query.return_value.filter_by.return_value.first.return_value = existing_user
with pytest.raises(ValueError, match="邮箱已被注册"):
create_user("[email protected]", "anotheruser")
def test_get_user_by_id_nonexistent_user(mock_db_session):
"""测试查询不存在的用户"""
mock_db_session.query.return_value.filter_by.return_value.first.return_value = None
with pytest.raises(ValueError, match="用户 999 不存在"):
get_user_by_id(999)
注意看关键细节:
- fixture 名称是
mock_db_session,和 Step 1 完全一致——它记住了 - 命名规范
test_{函数名}_{场景描述}被完整延续——它记住了 - 注释里甚至主动标注了"复用 Step 1 定义的 mock_db_session fixture"——它知道这个约定来自哪里
如果是旧机制,Step 3 大概率会生成 mock_session、test_invalid_email_format,和前两步的命名体系完全割裂,合并测试文件时会产生冲突。
---
四、什么场景下用它最值
不是所有任务都适合同线程续跑。用一个简单的决策框架来判断:
先问自己三个问题:1. 这个任务有多个步骤,且步骤之间有依赖关系吗?
→ 是:继续往下看
→ 否:不需要续跑,普通调用就够
2. 步骤之间需要保持命名/风格/约定的一致性吗?
→ 是:强烈建议用续跑
→ 否:续跑有帮助但不是必须
3. 任务链条超过 8 步,或者涉及超大代码库吗?
→ 是:谨慎使用,注意早期上下文可能被压缩
→ 否:放心用
适合用续跑的场景
- 多步骤重构:先分析依赖关系,再逐文件重构,最后验证——每一步都需要"知道前面做了什么"
- 迭代式功能开发:先搭骨架,再填实现,再加错误处理——命名约定要贯穿始终
- 跨文件依赖分析:分析 A 文件 → 分析 B 文件 → 找出 A/B 的耦合点——需要记住两次分析的结论
- 测试用例生成:和本文实测场景一样,命名一致性至关重要
不适合用续跑的场景
- 跨项目复用:Thread 里的上下文是项目 A 的,拿来做项目 B 会带来干扰
- 需要全新视角的 Code Review:有时候你就是需要一个"什么都不知道"的新视角来发现问题
- 任务链条超长:超过 10 步的任务,早期约定可能被压缩失真,反而产生幻觉
---
五、API 层面:怎么用代码复现这套机制
对于想在自己项目里集成这个能力的开发者,这是最关键的部分。
核心就是 thread_id 的传递:第一次调用创建 thread,保存返回的 thread_id,后续调用传入同一个 thread_id 即可续跑。
from openai import OpenAI
client = OpenAI(
api_key="你的密钥",
base_url="https://api.openai.com/v1"
)
Step 1:创建线程并发起第一次任务
thread = client.beta.threads.create()
thread_id = thread.id # 保存这个 ID,后续续跑要用
client.beta.threads.messages.create(
thread_id=thread_id,
role="user",
content="帮我为 user_service.py 生成单元测试,命名规范:test_{函数名}_{场景},mock 统一命名 mock_db_session"
)
run = client.beta.threads.runs.create_and_poll(
thread_id=thread_id,
assistant_id="你的 assistant_id"
)
print(f"Step 1 完成,thread_id: {thread_id}")
Step 2:在同一线程里续跑(直接追加消息,不创建新 thread)
client.beta.threads.messages.create(
thread_id=thread_id, # 关键:复用同一个 thread_id
role="user",
content="分析上面生成的测试,找出覆盖率不足的场景"
)
run2 = client.beta.threads.runs.create_and_poll(
thread_id=thread_id,
assistant_id="你的 assistant_id"
)
Step 3:继续在同一线程里补充边界用例
client.beta.threads.messages.create(
thread_id=thread_id, # 还是同一个 thread_id
role="user",
content="根据上面的分析,补充缺失的边界测试用例,保持与已有测试相同的命名规范"
)
run3 = client.beta.threads.runs.create_and_poll(
thread_id=thread_id,
assistant_id="你的 assistant_id"
)
print("三步任务完成,所有步骤共享同一上下文")
整个逻辑就 20 行出头,核心是:创建一次 thread,保存 thread_id,后续每步都传这个 ID。
---
📌 国内直连提示
>
上面的代码示例直接调用 api.openai.com 在国内可能遇到访问问题。
如果你想直接跑通这个续跑示例,可以把 base_url 换成稳定的中转地址:
>
> client = OpenAI(
api_key="你的密钥",
base_url="https://api.884819.xyz/v1" # 国内可直连
)
>
接口格式与官方完全兼容,thread_id 参数同样支持,换一行代码就能跑。
注册即送体验 token,国产模型(Deepseek R1/V3、Qwen3 等)完全免费,无月租。
地址:api.884819.xyz
---
六、现在就能用,不是"未来可期"
同线程续跑不是 AI 编程助手的"革命性突破",我不打算这样说。
它解决的是一个具体的、实际存在的协作摩擦:多步骤任务里,AI 助手的健忘症。这个问题在你做简单任务时感知不强,但一旦任务复杂度上去,它会成为一个持续消耗你精力的隐性成本——每次都要重新解释命名约定,每次都要粘贴前一步的代码,每次都在怀疑它有没有真的理解你的意图。
现在这个问题有了一个工程化的解法,而且接入成本极低:保存一个 thread_id,传进去,就这样。
今天就能试,今天就能看到差异。
---
但下一个问题自然就来了。同线程续跑解决了"记住上下文"的问题——但如果任务足够复杂,Codex 会不会在第 N 步悄悄覆盖掉早期的决策,而你根本没发现?
我在实测中碰到了一次这样的情况:第六步生成的代码,悄悄把第二步定义的错误处理逻辑改掉了,改法还挺"合理",如果不仔细对比根本发现不了。
这个问题有个名字,叫上下文漂移(Context Drift)。下一篇我会专门拆这个机制,以及怎么用 prompt 锚点来防住它。
如果你正在用 Codex 跑多步骤任务,这篇文章你可能比我更需要看。
---
本文由8848AI原创,转载请注明出处。关注8848AI,带你从零开始学AI。#AI编程 #Codex #OpenAI #单元测试 #API开发 #8848AI #AI工具 #Python开发