一、先把概念钉住:它到底是什么

ZCode 这类工具,本质上是一个 "大模型 + 工程化外壳" 的组合体。底层是 LLM(代码能力来自预训练 + 指令微调/RLHF),外壳则负责三件事:收集上下文、组织对话、执行副作用。很多人误以为"聪明"全部来自模型,但实际体验的差距,八成来自外壳做得好不好——同样的模型,上下文抓得准、工具给得对,出来的补全和改动能直接用;反之就是一堆看似流畅的废话。

我自己的理解是:把它当成一个 "读代码很快、但没有项目记忆的初级工程师"。它每次干活前都需要你(或者工具链)把相关文件、报错、约定塞到它面前;它不会主动记住你上周的架构决策,除非你用 AGENTS.md、规则文件或记忆机制明确告诉它。理解了这个定位,很多"它怎么又忘了"的困惑就消失了。

提示:不要问"这个项目怎么改",要问"这是 user_service.py,它依赖 db.py 的 session,现在要加一个软删除字段,请给出 diff"。上下文质量 > 提问技巧。

二、整体架构:四层结构拆解

从一次请求的视角看,这类系统大致可以分成四层,自上而下是:

值得注意的是,模型和工具是解耦的。模型只输出"我想调用 read_file,参数是 {path: ...}",真正去读磁盘的是编排层。这个设计让系统可以在不换模型的前提下升级工具,也让"给模型加能力"变成了配置问题而非训练问题。

三、上下文工程:把代码库变成模型能读的"证据"

模型的上下文窗口有限(哪怕 200K),把整个仓库塞进去既贵又会稀释注意力。所以实际做法是检索式组装:先用符号/关键词/向量召回一批候选片段,再按相关性、位置(定义 vs 调用)、新鲜度排序,最后裁剪到预算内。下面是我自己写的一个简化版召回示例,思路是 词法匹配 + 符号命中加权:

import re
from pathlib import Path

def chunk_file(path: Path, max_lines: int = 60):
    """按函数/类边界粗粒度切块,模拟真实的 chunking 策略。"""
    lines = path.read_text(encoding="utf-8").splitlines()
    chunks, buf = [], []
    for line in lines:
        buf.append(line)
        if re.match(r"^(def |class |@)", line) and len(buf) > 1 or len(buf) >= max_lines:
            chunks.append("\n".join(buf))
            buf = []
    if buf:
        chunks.append("\n".join(buf))
    return chunks

def retrieve(query: str, roots: list[Path], top_k: int = 5):
    query_terms = set(re.findall(r"\w+", query.lower()))
    scored = []
    for root in roots:
        for path in root.rglob("*.py"):
            for i, chunk in enumerate(chunk_file(path)):
                terms = set(re.findall(r"\w+", chunk.lower()))
                # 简单 Jaccard 相似度,真实系统会叠加 embedding 与符号引用图
                score = len(query_terms & terms) / max(1, len(query_terms | terms))
                if any(t in chunk for t in query.split()):   # 符号精确命中加权
                    score += 0.3
                scored.append((score, f"{path}:{i}", chunk))
    return sorted(scored, reverse=True)[:top_k]

真正生产级的实现还会加 抽象语法树(AST)感知的切块、调用图扩展(召回 foo 时把调用 foo 的地方也带上)、最近改动文件优先(git diff 里的文件权重更高)。这些"小聪明"累积起来,就是"它好像懂我项目"的观感来源。

提示:仓库里放一个 AGENTS.md(或 .cursor/rules 之类),写清构建命令、代码风格、禁区文件、常见坑。这是投入产出比最高的一件事,比换模型还立竿见影。

四、工作流程:从按键到 diff 的完整链路

一次典型的"修改任务"链路是这样的:

  1. 用户在编辑器里选中代码或描述需求,交互层收集显式上下文(选区、光标文件、打开的标签页)。
  2. 编排层补上隐式上下文(诊断信息、git status、检索结果、规则文件)。
  3. 组装成 messages 发给模型;模型可能直接给答案,也可能先发起工具调用。
  4. 编排层执行工具、把结果作为 tool 消息回填,再次请求模型,如此循环(agent loop)。
  5. 模型产出结构化的 diff 或文件写入请求,交互层渲染成可逐块接受/拒绝的视图。
  6. 用户确认后写盘,随后跑 lint / test,失败信息再作为新一轮上下文反馈回去。

用伪代码表示这个循环大概是这样:

def agent_loop(task: str, tools: dict, max_turns: int = 12):
    messages = [{"role": "system", "content": SYSTEM_PROMPT},
                {"role": "user", "content": build_context(task)}]

    for _ in range(max_turns):
        resp = model.chat(messages, tools=list(tools.values()))
        if resp.finish_reason != "tool_calls":
            return resp.text                      # 最终答案

        messages.append(resp.to_message())
        for call in resp.tool_calls:              # 可并行执行多个工具
            result = tools[call.name](**call.args)  # 受控执行,见下节
            messages.append({
                "role": "tool",
                "tool_call_id": call.id,
                "content": truncate(result, limit=8000),  # 防止上下文爆炸
            })
    raise RuntimeError("达到最大轮数,任务未收敛")

注意两个工程细节:工具结果必须截断(一个 test 输出几万行会瞬间吃掉预算),以及轮数要有上限(防止模型在"读文件—再读文件"里打转)。收敛判断通常还包括"连续两轮没有新工具调用""diff 已通过静态检查"这类启发式。

五、工具调用与受控执行:安全边界在哪里

Agent 的危险不在于它"想"做什么,而在于它能做什么。rm -rf、git push --force、把 .env 读进上下文再回显给用户,都是真实发生过的事故。所以成熟系统普遍采用白名单 + 权限分级 + 审计三层防御:

我个人的实践原则是:让"不可逆操作"永远留给人。比如数据库迁移、删除文件、发布上线,哪怕模型给出了一键脚本,我也坚持手动执行并 review。AI 提速的部分应该集中在"生成 + 验证",而不是"决策 + 执行"。

提示:给 Agent 的命令执行加一层"危险命令正则拦截"很值得(git push --force、sudo、chmod 777、curl | sh)。宁可误拦,不要漏拦。

六、怎么用好它:评测、迭代与个人工作流

判断一个 AI 编程助手"好不好",别只靠体感。我会维护一个私有评测集:攒 20~50 个真实任务(补全、重构、修 bug、写测试、读代码问答),每次换模型或改 AGENTS.md 后跑一遍,看一次通过率、需要人工修正的行数、总 token 成本三个指标。体感会骗人,回归测试不会。

在日常使用上,我总结了几条心得:

归根结底,这类工具改变的是"想法到可运行代码"的延迟,而不是工程判断力的价值。把它当成一个超强的、但需要你时刻校对的结对程序员,才能真正吃到红利。