Astra 写代码:我们到底在图什么?
2026年9月7日
我越来越确信,整个AI工程领域都在内卷(Neijuan,原指向内蜷缩,中文语境中形容投入持续增加、竞争不断加剧,但产出却毫无提升的体系)。这种现象在西方有时表现为「996乱象」[https://lucumr.pocoo.org/2025/9/4/996/]。Neijuan对应的英文术语是Involution(内卷),源自《农业内卷化》[https://en.wikipedia.org/wiki/Agricultural_Involution]一书。书中的农业内卷化指通过精耕细作提升单位面积产量,但人均产出却维持不变。
当下AI领域的发展,在我看来正是如此。
这让我想到了GPT 6 Astra。各方评价都显示,这是一个令人惊叹的模型,我实在挑不出太多毛病。它精通计算机操作,能理解图像与复杂议题,完成任务时更是坚持不懈。毫无疑问,这类模型终将以某种方式改变世界。
但至少目前,我还不知道如何将它用于实际软件工程。由于我在Twitter上的相关讨论引发了不少关注,我决定整理一下思路,分享它生成的代码是什么样的。
有人跟我说:「阿明,你应该搞个软件工厂试试!」这话我听过好几次了,于是借着Astra发布的契机,我周末搭了个小型软件工厂来测试。既然大家都用AI生成粗制滥造的3D游戏,那我不如用它做点有用的事。这个软件工厂完全交由模型自主决定工作流程,它可以自由管理上下文,在agent-notes文件夹中记录工作内容,还能派生子智能体协作。我的目标是什么?试试让Python支持虚拟线程与词法作用域。为此,我耗光了一整轮ChatGPT的token额度——大约40亿个token。35小时后,这个工厂没产出任何有价值的成果,也没让我学会如何优化这类工具的运作。
不过它生成了大量代码和提示词,我得以研究这些内容。我发现,Astra的行为模式和Sol以及更早的OpenAI模型截然不同。后来我在日常编程中使用Astra时,也遇到了同样的问题,说明这并非软件工厂模式导致的特例。
我怀疑模型的训练过程可能出了些「问题」:模型在完成长周期任务时会得到丰厚奖励,但写出「垃圾代码」却几乎不会受到惩罚。结果显而易见:Astra在生成3D内容方面能力惊人,还能长时间自主推进任务、生成新的工作内容。我曾让它逆向解析我的扫地机器人,过程和结果都令人赞叹。单从这一点看,它确实很酷!
我对Astra的第一个不满,源于它调用工具时的代码风格。Codex模型越来越依赖纯bash命令完成各类操作,最近几个版本的Codex底层工具甚至直接用sed等工具读取文件——只是通常你看不到这些操作,因为Codex会解析bash命令,识别后就自动隐藏了。但Astra……对Python有种近乎偏执的偏好?其实这也不算意外,毕竟更早的OpenAI模型偶尔也会用临时Python代码读写、处理文件,但Astra的使用频率实在太高了。
这里需要先声明一点:我曾参与CPython解释器的开发,所以这个项目对我来说有点「当局者迷」。但我可以保证,即使是在Pi项目的TypeScript代码中,我也见过它写出奇怪的Python式逻辑。不过我手头最充分的怪异代码案例,都来自我那个周末完全无人监管的「粗制滥造工厂」。
它写Python本身没什么特别,值得关注的是它写的Python代码类型。我收集了一些例子,你可以大致看看:
在Codex的底层工具中,我多次发现子智能体完全用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
某次对话中,智能体运行测试时遇到了「Bad file descriptor」错误,Astra随即决定以极简代码测试macOS下能否通过Unix套接字传递文件描述符:
/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更新:
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:先写好脚本,再用Bash运行Python,最后通过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)
更夸张的是,它会用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)
你可能觉得这很滑稽,但我对此有疑问。首先,这类代码对人类来说完全不可读,想追踪执行流程简直难如登天。尤其是当它放弃使用工具提供的编辑功能时,你只能靠查看最终产物的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代码」,开始渗透到本应存入代码库的正式代码中。这种写法确实更省token:上面两个单元测试,按所属类结构缩进后,比用ruff format格式化后的版本少用10%的token。
我认为当前有几种趋势在相互拉扯,把AI代码生成推向了矛盾的方向。模型的训练迭代速度越来越快,而且似乎正朝着递归自我改进的方向发展。模型的奖励机制可能综合了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");
}
这种代码风格在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)
# ...
这不仅不符合目标代码库的风格,说实话,它不该是任何代码库的风格。我完全理解不了模型为什么要这么写。
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小时才被我关停。期间它生成了7.5万行新增代码,还在不断输出。35小时里它消耗了约10亿个token,仅API成本就高达1200美元。它一共提交了79次代码,平均每次提交成本约15.5美元,智能体之间还交换了约1400条消息。
我真的不需要一个智能体对着一个提示词跑35小时。这种方式显然行不通,也得不到合理的输出。
显然,这么用提示词很蠢。但只要没人干预,它就会一直跑下去——更早的模型不会这样,就连Fable都没这么疯狂。只要你不小心给了它一个稍微复杂点的任务,它就会一直执行直到「成功」,哪怕耗光整个订阅额度。
这也是我目前不太信任这个模型的原因:它会提交垃圾代码,因此我需要投入更多精力审核。哪怕出错率很低,我也不希望这样。
在工具调用代码被优化得更省token、更「能干活」的当下,我不禁怀疑,训练过程中是否有足够的信号让模型学会「让人类能看懂代码」。在我看来,Astra生成的很多代码「客观上就是烂代码」——但这是基于人类的评判标准。或许对于完全由智能体编写、只需要智能体理解的代码库来说,这种代码反而是「客观上好的」。
这也让我越来越困惑:我们到底在做什么?这些新模型确实令人惊叹,但我越来越怀疑,它们的发展轨迹是否还适用于当下的软件工程流程。我之所以会问这个问题,是因为之前的模型已经能很好地辅助软件工程,在AI经济中实现了正向收益。但Fable和Astra的成本大幅提升,产出却没跟上。
事实上,对我这样的软件工程师来说,Astra和Fable不仅成本高得离谱,而且完全不适用。这可能是因为这些模型越来越面向其他人群:律师、3D艺术家、数学家,以及所有需要使用计算机的人。
或许作为附带成果,现在你能在周末用AI快速生成一个看起来不错的3D游戏;或许只要你不在乎代码质量,也能让软件工厂一直运行下去。
我相信自己迟早会适应这一切,但不得不说,这些变化实在太诡异了。
附记:说到诡异,还有件事我百思不得其解:这些模型在沙箱环境中运行,按理说无法与其他智能体通信,却为何都能找到同一个公共维基页面作为智能体间的通信草稿本?难道它们在训练阶段就「串通」好了,记住了这些未来可能有用的网络资源?