写于 2026 年 9 月 7 日
我越来越确信,AI 工程的全部都在内卷(Neijuan,意为向内卷曲)。在中国,它描述的是一种不断要求投入更多努力、竞争更激烈,却不改善产出的系统。它在西方有时会以996 那套荒唐事的形式出现。内卷对应的英语词是 “Involution”,出自《农业内卷化》。农业内卷化指农业不断加密投入,使每平方米产出提高,而人均产出不变。
这就是我现在对 AI 的感觉。
这就说到 GPT 6 Astra。无论从哪个角度看,Astra 都是一个极其令人印象深刻的模型;对此我确实没什么可反驳的。它擅长操作计算机,理解图像与复杂主题,并且会不遗余力地追求完成任务。它绝对令人惊叹;这类模型终将以某种形式改变世界。
但至少目前,我不知道该怎样把它用于真正的软件工程。这在 Twitter 上引起了不少关注,因此我想把自己的想法总结一下,也展示一下这东西产出的代码是什么样子。
我的垃圾工厂
“Armin,你应该开一家软件工厂!”我已经听过好几次这种说法了,所以决定在这个模型发布的周末办一座小型软件工厂,算是庆祝一下。既然人人都在做 AI 垃圾 3D 游戏,那我该拿它做点有用的东西。我的软件工厂被刻意设置成让模型全权决定工作流如何进行。它可以自行管理上下文,也可以在 agent-notes 文件夹里维护自己的记录,然后再派出子代理处理工作。目标是什么?假如我们有一个带虚拟线程和词法作用域的 Python 呢?于是,我在这件事上烧掉了相当于一次完整重置额度的 ChatGPT token,看来大约有 40 亿 token。35 小时后,工厂交付的东西毫无价值,也没有教会我怎样运营一家更好的工厂。
不过它产出了大量代码和输入提示,所以我有材料可以研究。而且,它表现出了我在 Sol 和更早期 OpenAI 模型身上不习惯见到的行为1。后来我用 Astra 做普通编程时也遇到同样的问题,因此这并不只是工厂实验造成的结果。
我怀疑训练过程里有些地方“出了问题”。模型会因完成长周期任务而得到巨大回报,但对“烂代码”的惩罚大概很少。显而易见的结果是,Astra 非常擅长生成 3D 内容,而且可以持续很久,在过程中自己想出要做的工作。我让它以相当令人印象深刻的方式逆向了不少扫地机器人的东西。所以,它当然很酷!
高尔夫式工具调用
我对 Astra 的第一个问题来自它用于工具调用的代码类型。Codex 越来越依赖“纯 Bash”来完成更多操作。过去几个版本里,原始 Codex harness 就使用 sed 等工具读取文件。你通常看不到它们,是因为 Codex 会解析这些 Bash 命令,一旦识别出来就将其隐藏。但 Astra……真的特别爱 Python?这倒不算惊讶,因为更早的 OpenAI 模型有时也会按需写 Python 来读取和操作文件;但对我来说,Astra 的使用频率实在过头了。
这里有一个重要声明:这个项目非常元,因为我曾参与开发 CPython 解释器。但我可以向你保证,我也见过这个模型在 Pi 的 TypeScript 代码里做出奇怪的 Python 行为。不过,最充分的异常代码证据来自我让它在周末、在我的垃圾工厂完全零监督下工作的那段时间。
它会写 Python 并不有趣;有趣的是它写出的 Python 的类型。我收集了一些输出,供你略作浏览。
用 Python 拼接字符串来修改 C 代码
在 Codex harness 中,我发现好几次子代理完全不用补丁工具,转而用 Python 手工操作字符串。
python3 - <<'PY'
from pathlib import Path
p=Path('Include/internal/pycore_intrinsics.h');s=p.read_text().replace('#define MAX_INTRINSIC_1 14','#define INTRINSIC_RETAIN_ANNOTATION_CELLS 15\n\n#define MAX_INTRINSIC_1 15');p.write_text(s)
p=Path('Python/intrinsics.c');s=p.read_text();idx=s.index('#define INTRINSIC_FUNC_ENTRY');s=s[:idx]+'''/* Hold every old cell until the compiler has published the entire site's new
capture. A replaced cell's finalizer may reenter module __annotate__. */
static PyObject *
retain_annotation_cells(PyThreadState *tstate, PyObject *holders)
{
if (!PyTuple_CheckExact(holders)) {
PyErr_SetString(PyExc_TypeError, "annotation holders must be a tuple");
return NULL;
}
Py_ssize_t size = PyTuple_GET_SIZE(holders);
PyObject *previous = PyTuple_New(size);
if (previous == NULL) return NULL;
for (Py_ssize_t i = 0; i < size; i++) {
PyObject *holder = PyTuple_GET_ITEM(holders, i);
if (!PyCell_Check(holder)) {
Py_DECREF(previous);
PyErr_SetString(PyExc_TypeError, "annotation holder must be a cell");
return NULL;
}
PyObject *cell = PyCell_Get(holder);
PyTuple_SET_ITEM(previous, i, cell == NULL ? Py_NewRef(Py_None) : cell);
}
return previous;
}
''' +s[idx:];s=s.replace(' INTRINSIC_FUNC_ENTRY(INTRINSIC_AWAIT_BLOCK, await_block)',' INTRINSIC_FUNC_ENTRY(INTRINSIC_AWAIT_BLOCK, await_block)\n INTRINSIC_FUNC_ENTRY(INTRINSIC_RETAIN_ANNOTATION_CELLS, retain_annotation_cells)');p.write_text(s)
p=Path('Python/codegen.c');s=p.read_text();idx=s.index('static int\ncodegen_annassign(');s=s[:idx]+'''static int
codegen_retain_annotation_cells(compiler *c, location loc, PyObject *captures)
{
Py_ssize_t pos = 0;
PyObject *binding, *holder;
while (PyDict_Next(captures, &pos, &binding, &holder)) {
ADDOP_NAME(c, loc, LOAD_CLOSURE, holder, cellvars);
}
ADDOP_I(c, loc, BUILD_TUPLE, PyDict_GET_SIZE(captures));
ADDOP_I(c, loc, CALL_INTRINSIC_1, INTRINSIC_RETAIN_ANNOTATION_CELLS);
return SUCCESS;
}
''' +s[idx:]
a=s.index(' if (conditional_annotation_index != NULL) {',s.index('codegen_annassign(compiler *c'))
b=s.index(' if (captures != NULL) {',a)
# Move lookup before conditional registration and retain old cells before anything changes.
lookupstart=s.index(' PyObject *captures = _PyCompile_AnnotationCaptures',a)
lookup=s[lookupstart:b].replace(' return ERROR;',' Py_XDECREF(conditional_annotation_index); return ERROR;')
s=s[:lookupstart]+s[b:]
setup=lookup+''' if (captures != NULL && codegen_retain_annotation_cells(c, loc, captures) < 0) {
Py_XDECREF(conditional_annotation_index); return ERROR;
}
'''
s=s[:a]+setup+s[a:]
needle=' ADDOP_NAME(c, loc, STORE_DEREF, holder, cellvars);\n }\n }'
s=s.replace(needle,' ADDOP_NAME(c, loc, STORE_DEREF, holder, cellvars);\n }\n ADDOP(c, loc, POP_TOP); /* release old cells after full publication */\n }',1);p.write_text(s)
p=Path('Include/internal/pycore_magic_number.h');s=p.read_text().replace(' Python 3.16a1 3709 (Checked deferred annotation closure capture)',' Python 3.16a1 3709 (Checked deferred annotation closure capture)\n Python 3.16a1 3710 (Retain replaced annotation captures until publication)').replace('#define PYC_MAGIC_NUMBER 3709','#define PYC_MAGIC_NUMBER 3710');p.write_text(s)
p=Path('Lib/test/test_block_annotation_captures.py');s=p.read_text();idx=s.index(' def test_typing_consumers');s=s[:idx]+''' def test_replaced_cell_finalizer_sees_complete_site_publication(self):
module=execute("""\\
events=[]
class V:
def __init__(self,n): self.n=n
def __del__(self):
if self.n == 0: events.append(__annotate__(1))
for i in range(2):
x=V(i) # bind x y
y=i
value:(x.n,y)
""")
self.assertEqual(module.events,[{'value':(1,1)}])
self.assertEqual(module.__annotate__(1),{'value':(1,1)})
''' +s[idx:];p.write_text(s)
PY
make -j1 > /tmp/block-annotations-build7.log 2>&1
Socket 高尔夫
在一次对话中,代理在测试里碰到“Bad file descriptor”,Astra 决定要用一种极度压缩的方式,看看 macOS 上能否通过 Unix socket 传递文件描述符:
/usr/bin/python3 - <<'PY'
import socket,os,array
for into in (False,True):
a,b=socket.socketpair();fd=os.open(os.devnull,os.O_RDONLY);b.sendmsg([b'c'],[(socket.SOL_SOCKET,socket.SCM_RIGHTS,array.array('i',[fd]))]);print('fds',a.fileno(),b.fileno(),fd)
if into:r=a.recvmsg_into([bytearray(1),bytearray(),bytearray(19)],socket.CMSG_SPACE(4),socket.MSG_PEEK|socket.MSG_DONTWAIT)
else:r=a.recvmsg(20,socket.CMSG_SPACE(4),socket.MSG_PEEK|socket.MSG_DONTWAIT)
print('peek',r,flush=True)
rights=array.array('i',r[1][0][2]);print('rights',rights,flush=True)
for f in rights:
try: print('stat',os.fstat(f))
except Exception as e: print('error',e)
r=a.recvmsg(20,socket.CMSG_SPACE(4),socket.MSG_DONTWAIT);print('consumed',r,flush=True)
a.close();b.close();os.close(fd)
PY
用 Python 修改代理笔记
代理笔记相当一致地由 Python 更新:
python3 - <<'PY'
from pathlib import Path
p=Path('agent-notes/live/block-with-bindings.md');s=p.read_text().replace(' has117/77/zero failures.', ' has117/77/zero failures; existing bundled Expat unreachable-fallthrough\n warnings are the only optimized warnings.')
# Keep the final evidence readable without rewriting historical parent requirements.
for a,b in [('all328','all 328'),('pass31','pass 31'),('pass all328','pass all 328'),('pass,9.2s','pass, 9.2s'),('log`,210','log`, 210'),('log`,5,731','log`, 5,731'),('log`:18/18','log`: 18/18'),('pass,88','pass, 88'),('pass,90','pass, 90'),('skips,1m','skips, 1m'),('all6,280','all 6,280'),('has117','has 117')]: s=s.replace(a,b)
s += '\nKey source review: Python/symtable.c:603 (discovery), :3985 (sequential header traversal),\nPython/codegen.c:3488 (source-only exclusion), :5836 (publication), :5853 (normal/\nunwind reference cleanup), :5925/:6037 (enter-protected target setup).\n'
p.write_text(s)
for name in ('STATE.md','build-and-test.md'):
p=Path('agent-notes/live')/name;s=p.read_text()
for a,b in [('build:117','build: 117'),('paths.18','paths. 18'),('paths.\n18','paths.\n18'),('and210','and 210'),('pass5,731','pass 5,731'),('All6,280','All 6,280'),('failures,31','failures, 31'),('in\n115s','in\n115s'),('have117','have 117'),('paths.\n18','paths.\n18'),('18 focused,210','18 focused, 210'),('and5,731','and 5,731'),('all6,280','all 6,280')]: s=s.replace(a,b)
p.write_text(s)
PY
git diff --check
git add -u
git add Lib/test/test_block_with_bindings.py agent-notes/done/asyncio-task-drivers.md
git diff --cached --stat
git commit -m 'Add explicit with and async with header bindings'
用 Python 运行 Node.js
有好几次,它用 Python 在另一台机器上启动 Node.js。它先写好脚本,再用 Bash 运行 Python,然后该程序再通过 prlctl 在我的 Windows 机器上运行 Node.js。
import subprocess
code = "const{readFileSync}=require('fs');const{strict:a}=require('assert');const c=require('C:/Users/mitsuhiko/AppData/Local/Temp/pi-clipboard-threads/win32-arm64.node');(async()=>{const p=c.getText();a.ok(p instanceof Promise);const saved=await p;const image=await c.getImage();if(image||saved===null){console.log('arm64 async text/image reads passed; preserving non-text clipboard');return}try{for(const text of ['café 日本語','', 'large'.repeat(200000)]){const p=c.setText(text);a.ok(p instanceof Promise);await p;a.equal(await c.getText(),text);a.equal(await c.getImage(),null)}console.log('Windows ARM64 async Unicode, empty, large text and empty image passed')}finally{await c.setText(saved)}})().catch(e=>{console.error(e);process.exitCode=1})"
subprocess.run(['prlctl', 'exec', 'Windows 11', '--current-user', 'C:\\Program Files\\nodejs\\node.exe', '-e', code], check=True)
用 Python 运行 Node.js,再运行 PowerShell
既然已经这么做了,它就用 Bash 运行 Python,再由 Python 运行 Node.js,最后让 Node.js 调用 PowerShell。
import subprocess
code = "process.env.PSModulePath='C:/Windows/System32/WindowsPowerShell/v1.0/Modules';require('child_process').spawnSync('powershell.exe',['-NoProfile','-NonInteractive','-ExecutionPolicy','Bypass','-File','C:/Users/mitsuhiko/AppData/Local/Temp/pi-clipboard-threads/pi-clipboard-windows.ps1'],{stdio:'inherit'});console.log('completed')"
subprocess.run(['prlctl', 'exec', 'Windows 11', '--current-user', 'C:\\Program Files\\nodejs\\node.exe', '-e', code], check=True)
你可以觉得这挺好笑,但我对此有些疑问。第一个问题是:人根本读不懂它。如果你想跟上发生了什么,那就祝你好运。尤其当它放弃使用 harness 提供的编辑工具时,你只能依赖最终产物的 diff 查看器,因为只靠读这些代码,几乎不可能在变更发生时理解它。
大多数时候,在 Pi 中情况没这么糟,因为我主要看到它用 edit 工具编辑。但它一旦带着子代理彻底发疯(代理觉得没人盯着它的时候),就会诉诸各种越来越离奇的行为。我其实不知道模型是否认为有人在看,不过这就是它给我的感觉。
接着,它开始把同样的胡闹写进真正会被提交的代码。我主要在测试中见过,但例如它在 HTML 中写内嵌 JavaScript 或 CSS 时也会出现。它似乎只要和常规代码“隔了一层”,就会落入这些模式。
下面是它生成的一些单元测试:
完全无视空白与缩进
def test_unpack_suspension_and_continuation_close(self):
from continuations import Continuation,suspend
readers=[]
class Source:
def __iter__(self):
yield 1
suspend('unpacking')
yield 2
ns=execute('''
def run():
a,b='old-a','old-b'
readers.append(lambda: (a,b))
def a,b=Source()
suspend('published')
''',Source=Source,readers=readers,suspend=suspend)
with Continuation(ns['run']) as continuation:
self.assertEqual(continuation.resume(),'unpacking')
self.assertEqual(readers[0](),('old-a','old-b'))
self.assertEqual(continuation.resume(),'published')
self.assertEqual(readers[0](),(1,2))
class Value:pass
refs=[];frames=[];callbacks=[]
ns=execute('''
def run():
for def x in [Value()]:
refs.append(weakref.ref(x))
frames.append(sys._getframe())
callbacks.append(lambda: x)
suspend('body')
''',Value=Value,refs=refs,frames=frames,callbacks=callbacks,weakref=weakref,sys=sys,suspend=suspend)
with Continuation(ns['run']) as continuation:self.assertEqual(continuation.resume(),'body')
self.assertNotIn('x',frames[0].f_locals)
self.assertIsNotNone(refs[0]());callbacks.clear();self.assertIsNone(refs[0]())
def test_ast_roundtrips_and_future_annotation_unparse(self):
source='callback=lambda {for def a, [b,*rest] in [(1,[2,3])] {return a,b,rest}}'
tree=ast.parse(source);node=tree.body[0].value.body[0]
self.assertIsInstance(node,ast.ForBinding)
self.assertEqual(node._fields,('target','iter','body','orelse','type_comment'))
self.assertEqual(node.lineno,1);self.assertGreater(node.end_col_offset,node.col_offset)
self.assertEqual(ast.dump(tree),ast.dump(ast.parse(ast.unparse(tree))))
ns=execute('from __future__ import annotations\ndef f(arg: '+source.split('=',1)[1]+'): pass')
self.assertEqual(eval(ns['f'].__annotations__['arg'])(),(1,2,[3]))
tree=ast.parse('async def f():\n async for def x in values: pass # type: ignored\n')
self.assertIsInstance(tree.body[0].body[0],ast.AsyncForBinding)
self.assertEqual(ast.dump(tree),ast.dump(ast.parse(ast.unparse(tree))))
所以,至少在某些情形下,它通常为了高 token 效率的工具调用而进行代码高尔夫的 Python 垃圾,会泄漏到它生成、且应当保存下来的 Python 代码中。嗯,它显然更节省 token。上面两段单元测试若按原本的类结构缩进,仍然比经过 ruff format 后的版本省 10% token。
只要你不看,它就是 AGI
我认为现在有几件事正在把整个领域推向彼此冲突的方向。这些模型的训练规模正快速加速,而且它们大概也在走向递归式自我改进。模型的奖励可能由 token 效率、任务完成率,以及圈复杂度等简单指标共同构成。但我们人类并不通过简单、可量化的度量来判断代码是否可读、可理解。这些东西都很容易单独测量,也可以在局部范围内优化。
但局部优化不会产生全局最优,而且看输出的人越少,这件事就越不重要。显然,我的软件工厂在运行约 35 小时后搁浅了,但从它留下的笔记中可以看到它逐渐滑向疯狂。例如,任务文件里的任务编号一开始乐观地是 1、2、3、5、5a,最后变成 8a、8a1,又演化成 8b2c2b3 和“8b2c2b2b checkpoint1”。它写出的代码越来越野。我不想用它试图构建的东西烦你,不过以下是解释器改动的一些例子:
到处都是硬编码常量
我不知道它从哪里弄来这些数字,但某个时刻,它开始把随机常量从一个模块传给 C 实现。起初,这个函数主要用于测试断言;但就在我关闭实验之前,非测试代码也开始依赖它。
static PyObject *
native_probe_run_impl(PyObject *callback, int sleep, int operation, PyObject *other)
{
pthread_mutexattr_t attr;
pthread_mutex_t mutex;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&mutex, &attr);
pthread_mutexattr_destroy(&attr);
pthread_mutex_lock(&mutex);
int previous = native_sentinel;
pthread_mutex_t *previous_mutex = native_mutex;
native_sentinel = previous + 1;
native_mutex = &mutex;
PyThreadState *tstate = PyThreadState_Get();
PyGILState_STATE gil = PyGILState_Ensure();
int saved_errno = errno;
PyObject *result = NULL;
Py_ssize_t value;
/* No intervening Python frame: these exercise ambient C provenance. */
switch (operation) {
case 0: result = PyObject_CallNoArgs(callback); break;
case 1: result = PyNumber_Add(callback, other); break;
case 2: result = PyNumber_Negative(callback); break;
case 3: result = PyObject_RichCompare(callback, other, Py_LT); break;
case 4:
value = PyObject_IsTrue(callback);
if (value >= 0) result = PyBool_FromLong(value);
break;
case 5:
value = PyObject_Length(callback);
if (value >= 0) result = PyLong_FromSsize_t(value);
break;
case 6: result = PyObject_GetIter(callback); break;
case 7: result = PyIter_Next(callback); break;
case 8: result = PyObject_GetItem(callback, other); break;
/* ... */
case 21:
result = PyType_Type.tp_call(callback, other, NULL);
break;
case 22: case 23: case 24: case 25: case 26:
result = conversion_probe(operation, callback); break;
case 27: case 28: case 29:
result = protocol_probe(operation, callback, other); break;
case 30: case 31: case 32: case 33: case 34: case 35:
case 36: case 37: case 38: case 39: case 40: case 41:
case 42: case 43: case 44: case 45: case 46:
case 47: case 48: case 49: case 50: case 51: case 52:
case 53: case 54: case 55: case 56: case 57: case 58: case 59:
case 60: case 61: case 62: case 63: case 64: case 65: case 66:
case 67: case 68: case 69: case 70: case 71: case 72:
result = collection_probe(operation, callback, other); break;
default: PyErr_SetString(PyExc_ValueError, "bad probe operation");
}
C 中在同一行多次调用宏
这种代码风格并不存在于 CPython 代码库中,却出现在新生成的代码里。
PyObject *info = PyTuple_Pack(3, name, mangled, suite->su_id);
PyObject *flags = PyLong_FromLong(DEF_LOCAL);
if (key == NULL || info == NULL || flags == NULL ||
PyDict_SetItem(suite->su_bindings, mangled, key) < 0 ||
PyDict_SetItem(st->st_cur->ste_block_bindings, key, info) < 0 ||
(private && PyDict_SetItem(st->st_binding_info, key, info) < 0) ||
(private && PyDict_SetItem(st->st_cur->ste_symbols, key, flags) < 0)) {
Py_DECREF(mangled); Py_XDECREF(key); Py_XDECREF(info); Py_XDECREF(flags);
goto error;
}
Py_DECREF(mangled); Py_DECREF(key); Py_DECREF(info); Py_DECREF(flags);
生产代码中的随机索引
就像操作符对应的数字一样,它也用列表中的随机整数来存放状态。
def _register_task(task):
"""Register an asyncio Task scheduled to run on an event loop."""
_scheduled_tasks.add(task)
if _task_accelerator is not None:
_task_accelerator[6](task)
def _register_eager_task(task):
"""Register an asyncio Task about to be eagerly executed."""
_eager_tasks.add(task)
if _task_accelerator is not None:
_task_accelerator[8](task)
def _enter_task(loop, task):
if (_task_accelerator is not None and
_task_accelerator[5]() is loop and loop not in _current_tasks):
return _task_accelerator[1](loop, task)
# ...
C 中丑陋的 tokenizer 代码
这既不是该代码库的编码风格,坦白说也不该是任何人的编码风格。我不理解模型为什么会被驱使写成这样。
static int
apply_layout(tokenizeriterobject *it)
{
PyObject *source = PyBytes_FromStringAndSize(it->tok->source.bytes, it->tok->source.len);
if (source == NULL) return -1;
PyObject *events = _PyPegen_tokenize_layout(PyBytes_AS_STRING(source), it->tok->filename);
Py_DECREF(source);
if (events == NULL) return -1;
PyObject *result = PyList_New(0);
if (result == NULL) { Py_DECREF(events); return -1; }
Py_ssize_t index = 0;
PyObject *first_pos = PyTuple_GET_ITEM(PyList_GET_ITEM(it->pending, 0), 2);
PyObject *last_pos = PyTuple_GET_ITEM(PyList_GET_ITEM(it->pending, PyList_GET_SIZE(it->pending)-1), 2);
PyObject *previous = NULL;
for (Py_ssize_t i = 0; i < PyList_GET_SIZE(events); i++) {
PyObject *event = PyList_GET_ITEM(events, i);
if (previous && PyObject_RichCompareBool(previous, event, Py_EQ) == 1) continue;
previous = event;
PyObject *token = layout_token(it, event);
if (token == NULL) goto error;
if (token == Py_None) { Py_DECREF(token); continue; }
PyObject *pos = PyTuple_GET_ITEM(token, 2);
if (PyObject_RichCompareBool(pos, first_pos, Py_LE) == 1 ||
PyObject_RichCompareBool(pos, last_pos, Py_GT) == 1) { Py_DECREF(token); continue; }
while (index < PyList_GET_SIZE(it->pending)) {
PyObject *old = PyList_GET_ITEM(it->pending, index);
int cmp = PyObject_RichCompareBool(PyTuple_GET_ITEM(old, 2), pos, Py_LT);
if (cmp < 0) { Py_DECREF(token); goto error; }
if (!cmp) break;
if (PyList_Append(result, old) < 0) { Py_DECREF(token); goto error; }
index++;
}
if (index < PyList_GET_SIZE(it->pending)) {
PyObject *old = PyList_GET_ITEM(it->pending, index);
long kind = PyLong_AsLong(PyTuple_GET_ITEM(old, 0));
if ((kind == NL || kind == NEWLINE || kind == INDENT || kind == DEDENT) &&
PyObject_RichCompareBool(PyTuple_GET_ITEM(old, 2), pos, Py_EQ) == 1) index++;
}
if (PyList_Append(result, token) < 0) { Py_DECREF(token); goto error; }
Py_DECREF(token);
}
for (; index < PyList_GET_SIZE(it->pending); index++) {
if (PyList_Append(result, PyList_GET_ITEM(it->pending, index)) < 0) goto error;
}
Py_SETREF(it->pending, result);
Py_DECREF(events);
return 0;
error:
Py_DECREF(events);
Py_DECREF(result);
return -1;
}
这里的失败情形似乎相当明显:模型被训练为让看起来也像代码的工具调用节省 token,而它有时仿佛把那类代码带到了不该去的地方:代码库。
一条提示词跑 35 小时
我不太确定该说什么,但这台垃圾机器一直运行了 35 小时,直到我把它关掉。在那段时间里,它净增了 7.5 万行代码,却没有停下。35 小时内,它烧掉约 10 亿 token,原始 API 成本合计约 1,200 美元。它产出了 79 个提交,折合每个提交约 15.5 美元,代理之间还交换了约 1,400 条消息。
说实话,我不需要一个代理为一条提示词运行 35 小时。这显然既行不通,也不会产出合理结果。
所以显然,这样提示它是愚蠢的。但一旦无人看管,它确实会持续下去,而早期模型不会。就连 Fable 也没有这么疯狂。当你不小心交给它一个稍微过大的任务时,即使烧完一整份订阅,它也会继续,直到成功为止。
这大致就是为什么我目前很难信任这个模型。它已证明自己会提交垃圾,因此我得投入更多审查。即便失败率相当低,我也不想要这样。
一次性代码与被提交的代码
在一个为工具调用而把代码优化成节省 token、“把事做完”的世界里,我怀疑训练过程中是否真的收到了足够信号,去奖励“人类能够理解发生了什么”。我会说,Astra 产出的不少代码在我看来是“客观地糟糕”。但这是按我的人类感受得出的客观糟糕。也许对于一个完全由代理编写、也只需要代理理解的代码库,它反而客观地好。
这就是我越来越认真地问自己:我们到底为什么要做这件事。这些新模型无疑极其惊人。但我越来越怀疑,它们正在走的轨迹是否还适合当下的软件工程流程。我之所以会问这个问题,是因为我感觉我们曾经在这些模型上找到一个相当不错的软件工程平衡点;那是 AI 经济中能够展示正向回报的部分。但 Fable 的成本高得多,Astra 的成本也高得多,我却不觉得结果对得起这些成本。
事实上,对 Astra 和 Fable,我感觉不仅成本高得离谱,而且作为软件工程师,它们也根本不适合我。大概是因为这些模型越来越是为其他人准备的:律师、3D 艺术家、数学家、任何会用 computer use 的人,等等。
而作为启用这一切的副产品,你现在可以在一个周末糊出一款看起来很惊人的一键式 3D 游戏。也许只要你不在乎代码,现在就能无限期地运行一座软件工厂。
我肯定会习惯的,但天啊,这东西真怪。
**后记:**说到怪事:这些模型身处沙箱里,按说没有办法和其他代理通信,怎么却能找到同一批公开 wiki来充当代理通信的便签本?它们是否在训练运行期间串通起来,记住了未来可能派上用场的互联网资源?
- 我应当澄清:我以前也做过类似实验。通常它们不会运行这么久,代理会留下一份也许不完美、但仍能消化的软件。↩