跳到正文
Hacker News· handfuloflight·· 8 天前AI 评分36

Ctx.traits:用声明式配置替代提示词工程来定义 AI 智能体行为

Ctx.traits

AI 导读

Ctx.traits 提出用声明式代码配置(trait)替代 Markdown 提示词来定义 AI 智能体的意图与行为,使指令可维护、可测试、可组合。作者认为把过多逻辑塞进文本文件会让模型难以遵循并自行消解矛盾,因此改用类似 CDK 的方式生成静态配置契约,并主张对上下文做有选择的揭示与隐藏,而非让所有智能体共享全部信息。

正文 · AI 翻译

和许多在 AI 驱动系统的早期时代工作的人一样,我一直在努力寻找一种令人满意的智能体方法。随着模型在解决问题方面不断进步,这些工具和方法往往感觉要么是一场盲目的赌博,要么是事后才想到的补充。

每一个新趋势或新模式都像是又一次与模型讨价还价的蛮力尝试,希望通过强制手段让它变得可预测。这些方法往往伴随着一长串要求,以及一种默认接受其缺点的默契,让我不得不持续在多个层面上照看和反复检查。

这个领域已经经历了提示工程、上下文工程、循环工程,现在是图工程。每一种都有其自身的怪癖和假设。挑战在于从每一种中汲取有效之处,同时不假装生产力在提升而实际上并没有。

我并不声称已经找到了正确的方法,但这个过程令人欣慰,而且结果对我来说感觉是朝着正确方向迈出的一步。

最终,归结为在我整个职业生涯中屡试不爽的同一件事:抽象与恰当接口之间的谨慎平衡,结合模型使用可用工具解决问题的能力。

提示 · 契约胜过散文

事情很快就崩溃了:从我刚开始使用智能体那一刻起,把所有东西都塞进满是散文的 Markdown 文件就是一个陷阱。我一直试图添加结构,但它总是变成与模型无休止的搏斗,以及一种从未让人觉得足够可靠、可以依赖的抽象。

很快就清楚了:把太多逻辑放进文本文件,并期望 AI 智能体像我一样去解读它,并不是个好主意。模型难以遵循文件中的所有指令,而且常常试图自行解决矛盾。通过这些精心编写的文本文件来跟踪组织和优化,成了一种测试和维护上的负担,尤其是在团队协作中。

这引出了第一个关键决策:构建可维护、可测试且声明式的指令。它们应该易于智能体理解,也便于人类推理。我的第一次尝试是配置文件方法,起初感觉类似于使用 Terraform。但很快证明它过于冗长,用起来也不愉快。

这促使我实现了一个类似 CDK 的解决方案,它仍然以静态配置文件作为合成契约。将逻辑定义为代码,感觉是唯一合理的出路。

export default trait("deep-refactor", {
    version: "1.0.0",
    name: "Deep Refactor",
    summary: "Refactor the codebase to a more structured and maintainable state.",
    metadata: {
        tag: ["first-party", "refactoring", "review", "multi-agent"],
    },
    intent: {
        require: [intent.BoundedRefinement, intent.PreserveOriginalBehavior],
        avoid: [intent.InterfaceWidening, intent.UnboundedLoop],
        block: [intent.StyleOnlyChange],
    },
    behavior: {
        tone: [tone.Technical, tone.Direct],
        verbosity: verbosity.Low,
        method: method.EvidenceFirst,
    },
});

这种声明式配置让 trait 的意图和预期行为对智能体清晰明了。对于人类来说,快速浏览代码就足以理解 trait 如何工作,更重要的是,能够自信地维护它。目标是让对系统的推理变得直截了当。

以这种方式定义 trait,使得开发过程的更多环节可以自动化。期望的行为和指令变得可扩展和可组合,从而能够跨项目复用方法,避免复制粘贴或提示词 hack。

上下文 · 揭示与隐藏的艺术

我清单上的下一个问题,是我最有经验、尝试过最多不同方法的一个。迄今为止发现的方法在纸面上听起来都是好主意,围绕个人处理问题的方式形成,但都缺乏易用性,或者缺乏对其结果可信赖的信心:

  • Swarm agents - 你陈述问题并将工作委托给一组代理,旨在将多种意见归纳为一个宏观视角的解决方案
  • Per-task compaction - 你在单个上下文窗口内处理单个问题,之后进行压缩,希望“重获”模型的智能,同时保留项目的高层上下文
  • Automatic compaction - 你持续在一个看似“无限”的上下文窗口中工作,希望代理足够聪明,能够将知识永久压缩并使其“永存”
  • Handoff - 你与单个代理(例如规划者)合作,并将其对问题的定义移交给另一个代理(例如执行者)来实际实现

在花费大量时间尝试这些方法后,我得出结论:谨慎处理上下文至关重要。这通常让我能够绕过不完美的抽象,并让模型在寻找正确解决方案时承担更多繁重工作。

尽管如此,我常常觉得上下文没有被充分利用,这似乎是一种错失的机会。接下来的领悟是,并非所有上下文都是平等的。你输入什么,就会得到什么。

决定揭示什么、隐藏什么以及何时这样做,是一门微妙的艺术。并非所有代理都需要知道一切,它们之间如何共享知识必须受到控制。这既关乎工具,也关乎纪律。

代理的输入 / 输出

你可能希望保留在上下文中的内容之一,是你试图做出的变更的输入和输出。为此,你需要在 trait 中声明 port.input 或 port.output。这些抽象使你的 trait 能够接收和返回数据。

const changeRequest = port.input.text("change-request", {
    description: "The change to make — a bug to fix or a feature to add.",
});

const changeTarget = port.input.text("change-target", {
    description: "Where the change applies — the repository path, module, or subject.",
});

const commitReport = port.output.text("commit-report", {
    description: "Final commit evidence from the `git commit` command step.",
});

代理的存储

你可能希望保留在上下文中的另一项内容,是一个或多个代理正在处理的工作中特定部分的结果。为此,你需要在 trait 中声明一个 slot。这些抽象允许你的 trait 存储数据,这些数据可以在任务运行期间在不同位置复用。

const workSummary = slot.text("work-summary", {
    description: "Worker's report of the state of the work implemented.",
    hint: "What work has been done, what was the approach and potential concerns.",
});

const reviewVerdict = slot.verdict("review-verdict", {
    description: "Reviewer's verdict on the work implemented.",
    hint: "Has the reviewer approved the work, or does it still need some changes?",
});

const commitReport = port.output.text("commit-report", {
    description: "Scribe's final commit, including title & content.",
    hint: "What was implemented, grounded in initial request and worker's summary.",
});

存储数据只是问题的一部分。强制其正确的形状同样重要。在运行时,无论 trait 如何被使用,port 和 slot 都要求数据遵循严格类型的 schema。对于 slot.text 等常见类型有辅助工具,但你也可以根据需要定义自己的 schema。

const verdict = schema.object("verdict", {
    status: schema.enum(["approved", "rejected", "needs-changes"]),
    blockers: schema.texts({ required: false }),
    advisory: schema.texts({ required: false }),
});

代理的资源

另一个关键考虑因素是代理可以使用哪些资源。这通常通过在提示或技能中直接提及文件、目录或产物来处理,但精确控制哪些资源可供哪些代理使用以及如何使用至关重要。没有这一点,模型很容易对自己应该知道什么以及应该基于什么做出决策感到困惑。

这引出了 trait 定义中资源的概念。资源是向代理传递数据的抽象,可以精确控制数据在何时以及如何被提供。

const reviewGuidelines = resource("review-guidelines", {
    path: "resources/review-guidelines.md",
    hint: "Review guidelines that reviewers score implementation against.",
    render: resource.render.Inline,
});

const codeStandards = resource("code-standards", {
    path: "resources/code-standards.md",
    hint: "Code standards that might be useful to workers.",
    render: resource.render.Reference,
    trigger: resource.trigger.OnDemand,
});

这让我能够控制资源是内联在上下文中,还是仅在需要时引用以供检索。我还可以指定资源是立即可用,还是仅在代理明确请求时通过资源发现工具可用。这种级别的控制有助于避免信息的意外泄露。

防腐蚀

最后,重要的是要防止特质被以不必要的数据过载上下文的方式使用。这与其说是关于具体的抽象,不如说是关于如何将它们结合使用来控制和优化上下文。

在这个例子中,slot 和 resource 被用来将智能体锚定在项目、其约定以及当前任务的上下文中。智能体还被指示规划工作,以便后续智能体无需每次都重新阅读相同的计划。

const featureRequest = port.input.text("feature-request", {
    description: "Feature to implement in the project.",
});

const productSpec = resource("product-spec", {
    path: "@docs/product.md", // Path relative to the project root.
    hint: "Specification of the project being worked on.",
});

const implementationPlan = slot.text("implementation-plan", {
    description: "Plan of the work to be done.",
    hint: "Detailed plan for the implementation of the feature.",
});

..

sequence.prompt("Implementation Planning", {
    title: "Build the implementation plan",
    text: "Create a detailed implementation plan for the feature.",
    agent: planningAgent,
    input: [featureRequest, productSpec],
    output: implementationPlan,
})

..

sequence.prompt("Implementation", {
    title: "Work on the implementation",
    text: "Implement the feature.",
    agent: workerAgent,
    input: implementationPlan,
})

..

这是上述机制的一个简单示例:请求和项目规范被加载一次,并编译成实施计划。到了实施的时候,工作智能体收到要遵循的详细计划。

这样一来,杂乱上下文的方法被一种确定性的方式所取代,用于在智能体和资源之间连接数据流。这为我专注于智能体如何以受控方式实际执行工作奠定了基础。

循环 · 掌控的舒适感

这个初步探索的最后一部分是循环。我推迟了一段时间才处理它,以为循环方法已经成熟且得到解决。这个假设并不成立。

在花更多时间使用较新的模型后,我重新审视了这个问题。不断委托给子智能体感觉过度了,我发现自己对过程中实际发生的事情控制力更弱。

我对工作并行化以及将多角度结果合并为单一解决方案所承诺的好处持怀疑态度。即使经过几次成功的尝试,我也觉得这种方法不尽如人意。

经过一番探索,我意识到要真正驾驭这个工作流,运行时本身需要拥有智能体协作的外层循环。这导致需要一组明确的控制功能:

声明顺序

顺序应以清晰、易于理解的方式声明。它应该读起来像一份清单,准备好被智能体迭代和执行。这使过程更容易推理和维护。

procedure([
    sequence.prompt("step-a", { text: "Execute Step A" }),
    sequence.prompt("step-b", { text: "Execute Step B" }),
])

声明智能体到顺序的分配

每一步都应由默认智能体执行,或明确分配给专门的智能体。不同的步骤需要不同的推理和专业知识。有意识地分配智能体有助于避免意外的错配。

procedure([
    sequence.prompt("step-a", { agent: workerAgent }),
    sequence.prompt("step-b", { agent: reviewAgent }),
])

声明顺序中各步骤之间的关系

必须清楚每个步骤与其他步骤的关系:哪些步骤继承结果、产生什么输出以及谁使用它们。这也让我能够自信地指定哪些步骤可以并行运行。没有这一点,过程将变得难以调试。

procedure([
    sequence.parallel([
        sequence.prompt("step-a", { text: "Execute Step A" }, output: slotA),
        sequence.prompt("step-b", { text: "Execute Step B" }, output: slotB),
    ]),
    sequence.prompt("step-c", { text: "Execute Step C" }, input: [slotA, slotB] }),
])

声明步骤之间的数据连接

精确定义每个步骤使用哪些数据至关重要。这确保每个步骤恰好收到它需要的内容,不多也不少。

procedure([
    sequence.prompt("step-a", { text: "Execute Step A" }, input: resourceA, output: slotA),
    sequence.prompt("step-b", { text: "Execute Step B" }, input: resourceB, output: slotB),
    sequence.prompt("step-c", { text: "Execute Step C" }, input: [slotA, slotB],
])

将确定性过程声明为步骤

这让我可以将一个步骤声明为特定的 CLI 命令,无需由智能体发起。我控制命令的输出何时进入该步骤的上下文,避免智能体上下文中出现不必要的数据。这有助于保持过程可预测。

procedure([
    sequence.command("git-stage",  { text: "git add -A" }),
    sequence.command("git-commit", { text: command.text`git commit "${commitMessage}"` }),
])

声明循环迭代退出 / 满足

明确说明循环迭代何时完成以及过程可以继续,这一点很重要。这让我可以微调工作和审查过程,确保智能体持续进行直到结果令人满意。没有这一点,循环很容易变得无休无止。

procedure([
    sequence.loop("step-loop", [
        sequence.prompt("step-a", { text: "Execute Step A" }, input: resourceA, output: slotA),
        sequence.prompt("step-b", { text: "Execute Step B" }, input: resourceB, output: slotB),
    ], {
        until: condition.all([
            condition.equals(slotA, schema.bool.True),
            condition.equals(slotB, schema.bool.True),
        ])
    })
])

将所有内容整合起来

有了这些功能,我现在可以为智能体定义复杂的工作流。这个过程在我内部运作良好,包括在构建 ctx.traits 本身时也是如此。它并不完美,但距离我想要的掌控水平又近了一步。

procedure([
    sequence.prompt("implementation-planning", {
        agent: planningAgent,
        input: [productSpec, planGuidelines],
        text: prompt.text`Create a detailed implementation plan for: ${featureRequest}`,
        output: implementationPlan,
    }),

    sequence.prompt("implementation-work", {
        agent: workerAgent,
        input: [codeStandards, codeConventions],
        text: prompt.text`Implement the change in: ${implementationPlan}.`,
        output: implementationWorkReport,
    }),

    sequence.loop("implementation-refinement-loop", [
        sequence.prompt("implementation-review", {
            agent: reviewAgent,
            input: [implementationPlan, reviewGuidelines],
            text: prompt.text`Review the implemented work report: ${implementationWorkReport}`,
            output: reviewVerdict,
        }),
        sequence.prompt("implementation-refinement-work", {
            agent: workerAgent,
            input: [codeStandards, codeConventions],
            text: prompt.text`Apply the reviewer findings for the change: ${reviewVerdict}.`,
            output: implementationWorkReport,
        }),
    ], { until: condition.equals(reviewVerdict, schema.Verdict.Approved) }),

    sequence.prompt("commit-message-writing", {
        agent: scribeAgent,
        input: [implementationPlan, implementationWorkReport],
        text: "Write the commit message for implemented work.",
        output: commitMessage,
    }),

    sequence.command("git-stage", {
        title: "Stage all changes for commit",
        text: "git add -A",
    }),

    sequence.command("git-commit", {
        title: "Stage all changes for commit",
        text: command.text`git commit "${commitMessage}"`,
    }),
]);

微调智能体

trait 是一份可共享的契约,既可以由个人跨项目使用,也可以由团队成员跨不同产品或在一个产品的不同领域内使用。

而配置则是我决定解耦的部分。这是一个取决于它在哪里运行或由谁运行的选择。

schema-version = "0.1.0"

[agent]
harness = "opencode"
model = "opencode-go/deepseek-v4-pro"
session-mode = "persistent"

[agent.narrator]
harness = "opencode"
model = "opencode-go/deepseek-v4-flash"
session-mode = "persistent"

[agent.role.scribe]
harness = "opencode"
model = "opencode-go/deepseek-v4-flash"

[agent.role.worker]
harness = "opencode"
model = "openai/gpt-5.6-terra-fast"
reasoning-effort = "medium"

[agent.role.reviewer]
harness = "claude-code"
model = "claude-fable-5"
reasoning-effort = "xhigh"

由此,我得到了一个解决方案,以声明式的方式将这些模式整合在一起,同时保持对外层循环的完全控制。这还不是终点。在用 ctx.traits 创建 trait 一段时间后,我意识到还有很多东西值得探索。

图 · 连接点滴

我从代数和编程两个角度理解图。它们的优势巨大,是表达关系、可达性和并行性的自然语言,我们解决的大多数问题都建立在它们之上。

我一直在关注 X 上关于图工程的近期讨论,寻找解决智能体问题的真正新方法。到目前为止,我看到的大多数内容都是我已经用上述概念解决过的想法。其中很多感觉像是把熟悉的模式重新包装,披上了新术语的外衣。

  • 将失败的工作路由到修订步骤的后向边是一个循环。
  • 根据审查者裁决而走的分支是一个条件。
  • 扇出到并行工作器并在末尾汇合是一个并行块。

到目前为止,我觉得采用这种方法会削弱验证过程如何建模的能力。我不认同任意控制流的想法,即任何节点都可以路由到任何其他节点,且拓扑结构可以在执行期间改变。以这种方式构建的工作流几乎无法在运行前进行审查,运行后也难以复现。

尽管如此,我仍然看到将图应用于智能体工作流的价值,但不是作为控制流的机制。这种连接性在特定领域、特定类型的任务和行为中有一席之地。

我一直在探索的第一个例子是一个会话抽象,它使智能体之间能够确定性地共享记忆,而不是按智能体或按步骤隔离记忆。

这形成了一张关于谁知道什么,以及何时知道的知识图。与任意控制流不同,它使运行更容易推理。我正在积极制作原型,努力寻找最佳结构,目标是在不久后将其集成到 ctx.traits 中。

组合 · 复用的节奏

我想在 ctx.traits 中解决的一个领域是如何与智能体一起复用经过验证的模式,同时保持对智能体能够可靠重复该过程的信心。

虽然可以创建技能并组合它们,但依赖 Harness 来选择正确的技能,或者必须自己记住确切的顺序,感觉效率低下且不必要地繁琐。

见识过函数式编程中组合的力量后,我开始设计一个系统,让 trait 和过程都能以有意义的方式组合,使它们易于提取和复用。

sequence.linear("implement-flow", [
    sequence.prompt("implement-work", {
        agent: worker,
        text: prompt.text`Implement feature based on specification: ${featureSpec}`,
        input: [implementReviewerdictA, implementReviewerdictB],
        output: [implementWorkBrief],
    }),
    sequence.prompt("implement-review-a", {
        agent: reviewerA,
        text: prompt.text`Review ${implementWorkBrief}, against spec ${featureSpec}`,
        output: [implementReviewVerdictA],
    }),
    sequence.prompt("plan-review-b", {
        agent: reviewerB,
        text: prompt.text`Review ${implementWorkBrief}, against spec ${featureSpec}`,
        output: [implementReviewVerdictB],
    }),
]),

我注意到我在好几个地方都写了这个序列,每次都有细微的差异。虽然实现工作和规划工作并不完全相同,但外层的流程几乎是一样的。由于序列是对朝着目标编排智能体的一种抽象,它可以被提取成一个函数。

const reviewedWorkflow = (settings) => sequence.linear(/* ... */);

这种方法让我们能够为不同的目标复用那套工作流模式,保持结构在各处一致。

const planWorkflow = reviewedWorkflow({ id: "plan", output: planBrief, /* ... */ });
const workWorkflow = reviewedWorkflow({ id: "work", output: workBrief, /* ... */ });
const testWorkflow = reviewedWorkflow({ id: "test", output: testBrief, /* ... */ });

export default trait("implement", {
    /* ... */
    procedure: procedure({
        /* ... */
        sequence: [planWorkflow, workWorkflow, testWorkflow],
    }),
});

有了这个,我们可以用可复用的流程来管理不同的智能体工作流,避免复制粘贴提示词或手动编排智能体。这能走多远取决于用户的需求;它可以被提取到一个团队或公司库中,跨项目使用,或公开分享。

未解决的问题 · 精力消耗

尽管这些模式在合适的工具下组合成了我喜欢使用的东西,但这个过程仍然让人感觉浪费,尤其是在项目之间切换时。

我发现,虽然我已经为自己解决了编写和运行工作流的问题,但在其他地方复用它需要复制粘贴文件或流程。这又带回了我在技能上遇到的同样问题:让所有东西保持同步成了一件苦差事。

依赖管理

一个技能很少单独存在,尤其是在团队中使用或以有用的、泛化的形式编写时。有了 trait,我想让这个过程容易得多,首先从 trait 的共享方式开始。

目前,我选择使用 npm 生态系统。由于 trait 是以代码形式编写的,安装一个包并在你的 .ts 文件中导入它是很直接的。

import { resources } from "@ctx-company/useful-traits";

..

// Import the same shared standards and review rubric.
// No need to copy-paste the files, just import them.
sequence.prompt("Implementation Review", {
    text: "Review the implementation.",
    input: [resources.codingStandards, resources.reviewRubric],
})

..

// Additionally, you have full control where resource is in your prompt.
sequence.prompt("Implementation Review", {
    text: prompt.text`Review the implementation, grade based on ${resources.reviewRubric}.`,
    input: [resources.codingStandards],
})

当 trait 在构建时被导入时,同步步骤会解析并内置它们,写入一个 lockfile。这确保不依赖 npm 的机制来防止导入的包发生变化,无论它是由作者拉取还是通过更新命令拉取。

我仍在琢磨该在多大程度上依赖 npm 生态系统。将来,我可能想减少这种依赖,但我更倾向于在看到人们实际如何使用它之前不做假设。

版本管理

一旦 trait 成为依赖项,它们就需要版本,更重要的是,需要一种方法来证明你和你的团队实际运行的是哪个版本。这种保证让你能够信任一个经过审查的 trait 保持不变,这在处理非确定性智能体时尤为重要。

这通过两种方式解决:映射到依赖项确切源修订版本的 lockfile,以及绑定到 trait 确切构建产物的校验和。

[[dependency]]
name = "@ctx-company/useful-traits"
version = "1.2.3"
resolved = "sha256:9f0d.." # checksum of the dependency's source

[trait]
name = "deep-refactor"
version = "2.0.1"
checksum = "sha256:a52d.." # checksum of the trait's built artifact

与依赖项一样,这个设计选择仍在演进中。我预计会随着生态系统的成熟重新审视它。

组合

我最初对这部分持怀疑态度,但它已成为我最喜欢的一部分。能够在整个生态系统中复用 trait 的部分是很有用的,我很想看看它会如何发展。

例如,一个共享的编码规范 trait 可以在你的团队中通用,而无需在每个项目的技能文件中引用它。智能体也可以作为预置的审查者或工作者被复用,而无需在每个 trait 中重新定义它们。

目前,它主要利用 trait 是代码这一事实来工作,因此你可以像往常一样在你的 .ts 文件中导入它们。

import { resources, agents } from "@ctx-company/useful-traits";

..

# Re-use the same shared standards and review-primed agent, across projects.
# Useful for ensuring all agents are all introduced to same standards.
export default trait("deep-refactor", {
    agent: agents.reviewer,
    resources: resources.codingStandards,
});

在不久的将来,我的目标是让 trait 可以作为子流程导入。这应该能让工作流各部分更自然地组合。

双向兼容性

我希望这个生态系统能尽可能兼容现有的 agent,因此我决定支持从 SKILL.md 和 AGENTS.md 导入和导出 traits。这应该能让现有系统与新系统之间的衔接更容易。

# Bring an existing skill in — typed with best effort.
# Traits are not approved and not runnable until you review them.
$ ctx traits import ./SKILL.md

# Render a trait back into eith SKILL.md / AGENTS.md
$ ctx traits render deep-refactor --profile agent-skills

这一点将在不久的将来得到改进。支持基于 MCP 的通信是一个优先事项,以将 traits 带出终端。

结论 · 尚未完成

目前,我意识到 ctx.traits 在职责范围上已经过于臃肿,需要拆分为独立的项目。这种臃肿是有意为之。我想弄清楚哪些部分应留在 ctx.traits 内部,哪些部分应被抽离出去。

这个项目只是长期路线图的第一层。作为我新公司的一部分,我正在规划更大的东西,目标是让 AI 驱动的工作真正变得愉快且高效。

如果这些内容引起了你的共鸣,请在下方订阅我的 newsletter,并在 X 上关注我。我会一路分享我的想法并记录我的历程。

感谢阅读!

Website · Docs · GitHub

来源:Hacker News · ctx.company