Codex 同线程续跑:AI 编程助手第一次真正"记住了你在干什么"

我让 Codex 帮我做第三步补测试的时候,它把第一步定义的函数名全写错了。

不是写错一个,是全错。test_create_user_with_valid_email 变成了 test_user_creationmock_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_sessiontest_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开发