我们开始吧。我叫Yonyi Michael,是初创公司Typedef的联合创始人之一。我们将与我们的创始工程师Brandon Calendarer一起举办这次研讨会。我们有两个小时的时间,我们会尽量让它变得有趣和好玩。我知道现在你们每个人都拿出了笔记本电脑,德国在世界杯比赛中输得一败涂地,但这正是我们的承诺:尽量让它变得有趣,这样你们就不需要去直播比赛了。
今天我们要讨论的是,在进入有趣的实践环节之前,先卷起袖子进行更技术性的研讨。我想简要介绍一下我们试图在Typedef解决的主要问题集,并为Brandon随后带领的实际研讨会中的概念和原则设定背景和提供上下文。
这次演讲的核心思想是Typedef的背景:我们正在构建数据智能体的信任层,使它们能够访问并在数据和系统上执行高复杂度的任务。演讲的标题是“数据上下文层”,这是我们为实现这一使命而构建的核心基础设施部分。
很多内容源于这样一个观点:也许你们中的一些人并不太深入数据领域,但一旦你尝试释放编码智能体或软件工程智能体来处理数据,你会发现这是一个更加微妙、复杂的环境。在构建正确的框架以及为这些智能体进行上下文工程时,有很多因素需要考虑,以确保它们真正有效。这就是我们要讨论的内容。
结论是:模型并不是瓶颈。模型的 capabilities 将持续增长,但让智能体真正有效的缺失上下文才是关键所在。这就是我们所说的“数据上下文层”。
作为Typedef这家初创公司,我们正在打一个赌。在过去两年里,我们大多数人都在行业内思考如何为编码智能体进行上下文工程。像Anthropic的CodeEx、Cursor这样的工具,让你能够理解如何有效地映射代码库进行检索,并为这些编码智能体设计工具。
然而,我们感到不足且开始看到更大需求的地方在于:同样的工作也需要为能够访问和操作数据的智能体完成。数据正是这样一个地方,如果智能体自信地出错(这种情况经常发生),可能会导致巨大的经济损失。
因为我们要求代理式分析执行的许多指标和计算,最终会出现在董事会演示文稿中、模型中的关键指标里,或者那些静默失败但具有极其重要特征和计算指标的管道中。
因此,我们的核心赌注是:智能体将成为这些数据系统的核心操作者。为了让他们真正有效,我们需要确保当他们执行读写操作时,能够安全、准确地完成这些任务,并大规模地信任这些结果。
这只有在赋予智能体推理层时才真正可行,这样它才能像拥有端到端工作知识的数据工程或数据团队一样,理解整个数据系统并对其进行推理。这就是这里“数据上下文层”的目标。
在过去一年半的时间里,我们在这个领域进行构建时发现,当今的编码智能体在尝试使其在数据方面有效时,主要在三个具体的领域失败。首先是影响分析:如果我做出这个更改,什么会受到影响?
其次是连接建议:在我的数据系统中进行这种连接是否安全?以及一般的安全代码更改,例如安全的转换逻辑更改。这次编辑的含义是否保持不变?它是否保留了该指标的意义?还是因为我的数据提交和变更,导致这个指标定义发生了变化?
这些不是边缘任务。它们是数据团队在日常工作中思考如何让企业数据与AI协作时的常规任务和流程。如果你的智能体不能可靠地推理和执行这些任务,很难真正信任它。
很多时候,甚至难以给它读写权限。而实际上重要的是让它们成为我们数据系统的核心操作者,而不只是问答类型的智能体。
这是我们通过工作定义和发现的核心差距:数据所需的上下文分散在许多不同的系统中。你的仓库存储行和实际数据本身;转换代码包含真正重要的业务逻辑;BI工具在其中存储指标、维度和一些业务定义。
此外,还有文档和Slack频道及线程中的部落知识,这些往往也存在于人们的脑海中。但智能体实际上需要介于这一切之间的东西。它需要了解一个模型的含义、它与其它模型的关联,以及做出某些更改时会破坏什么。
它还必须知道哪些操作是真正安全的。在接下来的15分钟里,当我讲解这些内容时,我们将使用一个称为“粒度”的术语。对于房间里不懂数据的人,“粒度”从数据团队的角度来看是什么意思?
简单来说,就是表的每一行实际代表什么。如果粒度是“按月划分的客户”,而我不小心将其改为“按周划分的客户”,那么之后的所有下游求和都会出错,即使SQL编译正确且一切运行正常。这就是我在谈论粒度变化时的意思。
为了让这一点在可靠性方面更加具体……
如果用户问智能体为什么某条数据管道的行数突然激增,一夜之间跳涨了40%,这意味着什么?智能体可以编写有效的SQL并运行它,甚至生成精美的图表。但最终它会报告错误的根本原因和对实际故障的错误分析。智能体可能会说:“看起来是数据量增加了。”但实际上,上游的一个列将一对一连接变成了一对多连接。这意味着数据的粒度发生了变化。
由于数据本身不包含粒度的信息,智能体无法区分真实的数据激增和实际的粒度变化。它可以运行查询、获取样本行或读取SQL以理解逻辑,但它不知道一行原本代表什么含义,而现在它代表了完全不同的意思。因此,仅有效的SQL是不够的,语义在让智能体有效工作时至关重要。
针对这种情况,可能有两种显而易见的修复方法,但事实证明它们都不奏效。首先可以给智能体代码,比如DBT或Airflow项目。但这意味着每次提示都要从头重建上下文,这受限于上下文窗口且具有概率性,且新会话需重新构建。
另一种方法是给予数据库访问权限,例如连接Snowflake或Databricks的MCP接口。智能体可以看到样本数据并读取数字,但它并不真正理解每一行数据的实际含义。因此它能读但不能解释。
第一种情况是智能体能读取代码,但没有记忆来持久化你在反复交互中花费大量时间建立的上下文知识;第二种情况是它能看见数据,却无法关联其背后的含义。两者都缺乏对系统行为方式的持久模型,而这正是我们要解决的核心使命。
让我们退一步思考:为了让智能体有效推理复杂的变更,你需要提供哪些缺失的上下文?有四个决定答案正确性的事实通常并未记录在任何地方供智能体读取或推断。第一,表中的一行究竟代表什么?这就是我们之前讨论的粒度问题。
第二,连接是否安全?第三,指标究竟意味着什么以及它们之间的关联关系如何?在数据系统中依赖关系非常重要。智能体虽然能很好地阅读SQL,但在最需要收集精确上下文的查询时刻,它很难轻易看到这四个关键要素。
我们的目标是让这些事实变得可计算和可查询,以便智能体能在需要时获取所需的上下文,而不必在每个会话中重新构建。接下来的幻灯片将介绍我们如何实现这一目标。以连接为例,这是第二个关键事实,也是语法不足以让智能体有效的最佳例证。
这里发生了一个等值连接,写法固定但根据底层数据可能有四种完全不同的结果:它可能保留行数、查找维度,或在从一对一变为一对多关系时 silently 复制每一行。类似地,SQL中常见的Between子句也可能导致安全的查找或爆炸粒度的日期骨架生成。
智能体能看到语法,但无法理解该语法会导致哪种场景结果。这并非模型或能力问题,而是上下文缺失的问题,我们需要武装智能体以弥补这一缺陷。第四个事实涉及理解哪些数据资产相互关联。
例如,了解哪些模型属于收入领域,哪些属于产品领域。这通常与试图访问这些核心指标的人员角色相关。如今团队常按文件夹名称组织,但这往往是依赖口耳相传的知识来理解模型与业务单元的关联。
但我们可以通过数据结构发现更多:查看模型的连接方式、表级或列级的血缘关系、共享键以及命名规范。系统会自动将它们聚类到不同领域,使智能体能直接查询“收入领域中有哪些模型”,而无需与人类反复确认。
这体现了上下文工程的原则:将存在于个人头脑或文档中的口耳相传知识,转化为可计算且可供智能体查询的形式。那么如何生成这些事实呢?我们的核心原则是尽可能保持确定性,仅在解决歧义或发挥模型优势时才使用LLM。
我们可以直接从SQL本身计算粒度和血缘关系。
实际上,我们会提取所有的转换逻辑,并将其解析到底层结构,以真正理解数据的实际组织方式。这样做的目的是确保智能体不会偏离实际的实现本身,因为图的基础和我们提供的上下文都直接源自代码实现。此外,你可以利用这种基于实际数据的验证机制来检查连接安全性(Join Safety),例如确认连接键是否真的唯一。这是通过对数据仓库中的表进行概要分析(Profiling)来实现的,从而让智能体不再需要猜测基数等模糊信息。
对于具有人类可读性的部分,例如理解业务名称、总结模型的用途或消除前两层逻辑中的歧义,大语言模型(LLM)就能发挥关键作用。这些事实会被计算出来并与实际数据进行核对,最后由 LLM 为图上的数据资产命名。这种分离至关重要,因为它确保了核心事实与实际实现及数据本身紧密绑定,防止智能体偏离核心逻辑。
这一切都存储在我们所谓的“数据上下文层”中。本质上,这是一个图结构,它只计算一次,并在每次代码提交时原子性地刷新。随着数据团队不断向仓库中提交新的数据变更,我们会增量地修改这个上下文图,使其始终代表生产环境中企业数据的最新状态。
之所以选择使用图结构而不是简单的文本堆砌或 Markdown 文件集合,是因为访问模式的需求。智能体能够通过一次遍历精确提取所需的子图,从而避免了提示词填充(Prompt Stuffing)或上下文过载的问题。这使得系统能够轻松扩展至超过 500 个模型,因为无需在每次构建上下文层时重新读取整个项目。
过去几年里,我们见证了图数据库的巨大复兴。我一直好奇其核心效用何在,直到发现 AI 模型非常擅长抓取上下文、编写查询并遍历图结构。由于数据资产本质上是基于依赖关系的,因此非常适合用这种图中的类型化对象、节点和边来表示。
(抱歉,大家可能看不到幻灯片了……灯光调暗一点就好。)这张图展示了一个子图的样本。你可以看到,边(Edges)携带了大量的血缘信息,定义了数据资产之间的不同关系类型,例如“由……物化”或“具有语义定义”。节点则代表不同的数据资产类型,比如表示逻辑代码的语义视图(Semantic View)或物理表。
这种上下文层使得智能体能够解决几个关键用例,首先是影响分析(Impact Analysis)。当智能体提议修改模型粒度时,在编写任何 SQL 之前,它会沿着图向下游走,询问每个下游依赖节点:“这个变更是否会破坏你当前表示的粒度?”例如,如果将月度粒度改为周度,智能体能检测到月度的 AR(年度经常性收入)指标会在每周行中重复计算。
由于上下文图涵盖了从源系统到 BI 仪表盘层级的整个数据体系,智能体能够追踪全生命周期的血缘关系。关键在于这一切都发生在“飞行前”(Pre-flight)阶段,即在代码构建或生产事故之前。智能体在行动之前就能知道影响范围(Blast Radius)。
第二个用例是连接推荐(Join Recommendations)。例如,当智能体试图按营销渠道归因 ARR 时,它不会仅凭列名盲目选择路径。相反,它会通过概要分析验证每个连接键是否唯一,并基于实际数据给出安全的连接路径建议。它会标记出那些会导致行数膨胀(如 7 倍)或静默丢失数据行数的连接方式,确保建议基于实际推理和数据证据。
第三个用例是安全的代码变更。通常业务定义嵌套在 SQL 中,当智能体尝试整理某个度量指标时……
从语法上移除谓词,SQL 仍能编译通过。但这看似无害的操作实际上改变了业务定义的核心部分。例如,这里悄悄重定义了“付费”指标的含义,导致下游三个仪表盘中该指标的包含范围(如试用流失率)被错误地扩展。上下文层能在这些业务定义和指标变更实际生效前捕捉到它们,将其作为智能体的首要约束条件。这不仅仅是 YAML 文件中的注释,而是区分语法重构与人类可信赖变更的关键所在。
那么这套方法真的有效吗?构建任何智能体系统时,必须能够衡量其表现。我们采用评估驱动开发模式,创建了一系列从简单到极难的真实生产数据工程用例、任务和流程。每个任务在隔离的数据环境克隆中多次运行,以防止跨环境的泄漏。我们通过通过/失败检查实际输出,并辅以 LLM 作为裁判进行影响分析等评估。
关键发现是:在模型和任务相同的情况下,唯一变量是智能体是否拥有数据上下文层的访问权限。当具备正确的上下文时,智能体能推理复杂的变更,任务成功率大幅提升近 35 个百分点。随着我们协助更多团队定制用例,这一基准测试将持续演进,但信号是一致的:提供更丰富上下文带来的效果提升远超更换模型或增加通用工具。
最后总结五个关键经验,即使不构建完全相同的系统也极具参考价值:第一,尽可能保持确定性。不要将整个 DBT 项目或 Airflow 逻辑直接扔给模型去处理复杂任务,而应通过解析深入理解转换逻辑的结构、关系和依赖。对于总结或分类模型等任务,再使用大语言模型进行处理。
第二,存储证据而非结论。保留信号让智能体自行推理往往更好,因为模型具备强大的查询时推理能力。第三,遍历优于提示词堆砌或上下文填充。不要一次性转储整个仓库,而是让智能体按需拉取子图并进行遍历,从而更高效地解决问题。第四,掌控工具表面。经过筛选 curated 的工具集优于现成的 MCP 大杂烩,甚至需要重写部分标准工具以匹配实际需求。
第五,重视评估(Eval)。无法衡量就无法建立可靠的智能体或从中学习。必须尽早并持续投入构建评估基础设施,将其作为软件中不断维护和更新的有机部分。至此结束我的分享,接下来进入实操环节。
我们发现智能体在数据工程任务中失败的原因并非无法读写 SQL,而是缺乏对数据含义、流向及变更影响的理解。若将上下文作为一等公民提取、计算并持久化,智能体便能通过图结构高效查询。这标志着从“仅读取代码”到“真正操作数据系统”的跨越,正是我们构建数据上下文层的目标。
感谢聆听我的简短发言。现在我将把时间交给 Brandon,他将带领大家进行实操工作坊,深入探讨我们刚才提到的概念。由于需要预留充足时间给 Brandon 完成整个工作坊演示,现场暂不提问。但我会在会后随时待命,欢迎大家稍后与我交流探讨。谢谢大家的掌声。
大家好,今天过得怎么样?你们表现得很好。太棒了。(抱歉,请给我一秒钟。)好的。那么,我今天要讲的话题肯定与之前Yonyi所谈论的内容相关。我们将尝试将这些概念整合起来,以便在剩余的时间里进行实际操作演示。我是Brandon,来自Typedef。(你们本周将在会议上听到很多演讲,主题都是“智能体的表现取决于你赋予它的上下文”,以及构建这种上下文实际上才是真正的工作。因此,这基本上是针对这一相同理念的实际操作课程。)在接下来的90分钟里,我们将一起为一个编码智能体构建一个上下文层——这是一个真实的例子,基于一个真实的项目代码库。今天我们将使用Quadrant的代码库。(稍后我会展示最后一部分,即即使你构建了这些令人惊叹的工具,如果不在过程中给予辅助,智能体大多时候还是会忽略它们。真正的工作在于如何让它实际使用这些工具。)
在深入之前,如果你想跟随操作,这个二维码链接到我的GitHub仓库,里面包含了所有相关代码。该仓库有几个Jupyter笔记本供人们跟随操作。再次提醒,如果你不想设置环境也没关系。那里的依赖项包括用于运行Neo4j的Docker Compose或Docker。(如果你想现场尝试其中的LLM部分,可以使用OpenRouter密钥。但这并非必需。如果你愿意,所有代码都可以离线运行;或者你也可以直接跟随我在这里的构建过程。)我会把这个二维码多展示一会儿,以便大家有时间设置环境,然后我们将直接进入笔记本内容。好了,大家都扫码了吗?准备好了吗?再给大家一分钟。(好的,完美。)
那么,让我们从每个编码智能体的起点开始:文本搜索。我要询问一个名为“posting list”的结构体类型,这是Quadrant中真实存在的类型。当智能体运行时,它会执行grep命令。(你会看到它找到了两个完全匹配的答案,都是“posting list”。但你会发现还有十几个相似项。比如我们有PostingListView、PostingListIterator等等,诸如此类。)你的智能体也会做同样的事情。它会进行grep搜索并得到这一大堆结果,然后从中选择一个。它可能选对,也可能选错。它会先探索其中一个选项。(也许它需要回溯、读取下一个结果才能弄清楚你真正想要的是什么,对吧?但它实际上没有任何方法能知道你想要的是哪个PostingList。文本本身不包含任何关于身份的概念,也不知道哪一个PostingList才是真正有意义的。)
你所关心的东西——比如哪些方法使用了它、它在何处被使用、它属于哪个crate(Rust中的包)——这些并不仅仅存在于文本中,而是存在于代码的实际结构中。(刚才我们听了Yonyi关于数据提出的非常类似的观点。让智能体在没有记忆的情况下,每次运行都重新遍历整个系统是有问题的。)首先,这会很昂贵。因为它每次都必须重新读取相同的一组文件。(如果你问它同一个问题五次,它就会回去把那同样的文件再读五遍。)但更重要的是,它是脆弱的。当它再次读取那五个文件时,下次可能会以不同的顺序读取它们;或者可能先读到一个给出错误答案的文件,从而引导它去另一个文件进行无谓的搜索。(因此,当我们试图为这类智能体工程化上下文时,我们真正要解决的是确定性和脆弱性的问题。)
目标不一定是让智能体变得更正确或更高效,尽管有时确实如此。真正的目标是让它变得更加确定。(因为我们试图将尽可能多的确定性应用到这个非常非确定的系统中。)如果没有这种持久且可信的代码模型,它每次都会靠猜测。我们可以看到,随着时间推移,模型在猜谜方面变得越来越好。(大家都能感觉到,一年前运行Claude Code的体验与现在完全不同。)所以,如果我们想为代码构建一个上下文层,我们基本上要遵循完全相同的配方,只是在稍不同的层面上操作。我们将缩小规模以构建这个研讨会,让你能带走一些今天就能运行的东西,而不是试图在这里复制我们整个产品。(因此,我们将采用“提取、持久化并查询”这一概念,开始从我们的代码库中提取信息。)
我们将拉取关于符号的信息——每种类型、每个特征(trait)、每个函数,以及它们的真实身份和位置。我们将获取这些不同符号之间的关系——在我们的图中形成边(edges),以便遍历。(这些信息只计算一次,但会被查询多次。这与我们在处理复杂数据架构时采用的整体方法相同,只是这里使用了一个足够小的示例以便在会场内运行。)这就是我之前所说的内容。今天我们将搭建它,这是一个你在会议期间会经常听到的概念。(我觉得这很酷。我做了关于“三层上下文”的幻灯片,结果查看日程表发现明天有人要做关于“四层上下文”的报告。所以我感觉有点被抢先了一步,但我们今天还是要讲这个,因为我认为这对于当前的语境来说仍然非常有用。)
你可以将编码智能体的上下文思考为不同层级。我是这样想的:第一层就是你刚才看到的,即grep、glob原始字符串,没有结构。(第二层是你获得大多数当前代码RAG解决方案的地方。你进行语义搜索,索引代码,嵌入它;或者做一些不错的词法搜索。)
语义搜索会返回按相关性排序的结果列表,即该实体被提及的所有位置。但最终这仍然是一个扁平的列表,虽然它提供了更相关的结果,本质上仍是线性的,无法展示不同代码元素之间的关系。因此,我们认为第三级(最高层级)应当是一个可遍历的图结构。这种结构将符号与真实身份链接起来,并在它们之间添加类型化的边,从而允许实际遍历。
在接下来的工作坊环节中,我们将逐步构建这些不同层级,最终实现一个能够利用该可遍历图来回答关于代码库问题的智能体。让我们先梳理一下整体框架。在构建过程中,有几个关键的阶梯或步骤才能达到第三级的理解深度。第一级是语法层面:首先必须解析你试图理解的代码。最简单的方法是使用语言无关的工具,如 Tree-sitter,它能提供代码库各部分及其交互关系的基础抽象语法树(AST)分解。
然而,这一层级仍无法处理命名冲突问题。例如,“posting_list”符号在 posting_list.rs 和 quant.rs 中虽然同名,但代表两个不同的实体。在第一级它们会发生碰撞。因此,关键在于第二级的“解析(Resolution)”。这是大型语言模型难以独立完成的任务,它能将两个模糊的 posting_list 转化为两种 distinct(不同且可识别)的类型。
对于 Rust 而言,这有点“作弊”,因为编译器会免费完成大量此类工作。我们将基本使用 rustdoc JSON 来开箱即用地获取这些信息。这确实是一种捷径,我特意选择 Rust 是因为需要快速构建这个工作坊;为另一种语言从头构建解析器或相关工具会困难得多。
不过,最后我会讨论一个更有用的 Python 案例,仓库中也有相应的示例笔记本供各位查阅,如果今天时间不够的话。下一层——即第二级(Rung Two),涉及我们提取的这些内容的实际含义。目前有一个笔记本,不确定今天是否能讲完——它展示了如何构建这一层。前两层完全是确定性的:我们力求以尽可能确定的基础方法来建立理解,使用 Tree-sitter 和 rustdoc JSON。
如果有时间,我们将探讨如何构建第二层:如何将信息输入大语言模型(LLM)进行分类、总结,发挥 LLM 擅长的功能,而不是简单地将所有数据扔给它让它自行理解。好,现在我们要打开笔记本 01……哦,它已经在这里了。大家能看到吗?或者我已经投屏到你们那边了吗?太好了。
这个笔记本将演示如何为 Quadrant 项目构建不同层级的上下文。今天不会索引整个代码库,因为它太大了。我们将查看大约五个 crate 的样本,这为我们提供了良好的测试代码语料库。我们可以开始运行这些单元格了。首先进行一些设置,现在设置已完成,让我们看看……是的,它在 GitHub 上。我可以快速再放一次链接。
如果不想在本地跟随操作,每个部分都有 Colab 链接。是的,对于笔记本一,如果你想使用图数据库功能,你需要一个 Neo4j 账户或登录权限。这只是该特定部分的要求。好的,我们可以看到这里有一些额外的代码用于在 Rust 本身内部执行解析操作。我今天不一定详细讲解这些,但仓库里都有供你们闲暇时查看。
今天我们要用一个神奇函数 extract_rust_gates,它将从对 Rust 代码库运行 Tree-sitter 的结果中提取信息。另一个函数则从 rustdoc JSON 中获取相同类型的信息。我们可以看到,从前几个 crate 中提取出了许多来自 Tree-sitter 的符号,以及 rustdoc JSON 解析后的符号版本。
现在我们可以运行一些代码来理解为什么我们要区分第一级和第二级。当我们在第一级询问关于 posting_list 的内容时,我们会看到导出版本,但不知道它究竟源自何处,无法逆向解析到其实际定义位置。这就是第二级的作用:它能指出这是 posting_list,其规范位置在……以及实际路径和行号。
这使我们能够确定当事物被重新导出到 crate 外部时,将其链接回原始 crate。一旦我们构建出图本身,智能体就能遍历这些不同元素之间的所有关系。现在我们将快速构建之前提到的第二级索引:我们收集所有符号,并让一个函数执行全文搜索。
搜索结果与之前看到的类似问题:我们可以搜索 posting_list 并获得非常有用的信息,看到这是重新导出、结构体等。但它仍然不会告诉我们这些不同元素之间的链接关系。智能体可以推断出这一点,并……
如果发布列表是一个结构体,它很可能在这里被重新导出。这大概是其基础所在。但你不想让模型每次都要进行这种逻辑跳跃,因为总有那么一次它会出错,从而导致结果完全偏离。因此我们的下一步是构建实际的图本身。我们将刚才提取的所有不同符号和信息转化为节点和边,并将其填充到我们的 Neo4j 图中。然后我们加载这些数据进入图数据库。
一旦数据在图中,我们可以查看 Neo4j 中的内容,看到一个漂亮的图表,展示了代码库各个部分之间的引用关系。既然有了这种图结构,我们就可以构建专门利用图能力的工具,让智能体更容易地完成任务。例如,如果我们想查看某个函数的变更影响范围——即修改该函数会导致什么发生变化或破坏——我们可以构建一个“爆炸半径”工具。
通过传入某个项目的限定名称,即规范名称,该工具会遍历图中定义的边,找出哪些内容依赖于该项目,以及如果该项目被更改会发生什么。在这种情况下,你可以让智能体回答那些原本需要跨多个文件进行多次查询才能解决的问题。理论上,智能体可以一次性给出答案。当然,这并没有立即奏效,毕竟现场演示总是充满挑战的。
我们可以看到,当我们计算该结构体的爆炸半径时,它能通过返回该方法的方式找到链接点。我们可以清楚地看到所有仅凭图内部内容就能建立的关联关系。最终,我们解决了命名冲突问题。如果我们有两个不同的发布列表并想查看它们的差异,我们可以分别调用新工具的爆炸半径功能,得到不同的结果集。
更改一个发布列表所引发的变化级别与影响另一个发布列表时并不相同。尽管这些项目拥有相同的底层名称,但对它们的规范解析使我们能够更明确地判断破坏某项内容时会发生什么变化,这比单纯读取所有文件并试图猜测要准确得多。现在我们可以利用已有的成果构建一个非常小且受限的函数智能体,以实际使用这些新工具。
有了这个爆炸半径工具后,我们可以快速搭建一个简单的智能体。这是一个基于 Pydantic 的智能体,配置了系统提示词,告知它拥有运行不同工具的能力。我们为其编写了一些基本工具来使用爆炸半径功能,并通过基本的 Cypher 查询来获取信息。一旦数据在图中,编写提取数据的查询就相当简单了。
如果你能表达出想要执行的想法,那么用 Cypher 或任何图数据库的等效语言编写查询以提取数据就非常容易。这也表明你为任务创建了正确的模式。现在运行这个智能体,它会在几次交互中调用工具并尝试理清思路,然后回答我们的问题。例如输入“如果我更改发布列表会破坏什么”,它会列出每个依赖符号及其所在的文件和行号。
基于爆炸半径的发现,它能准确告知哪些内容会被破坏,并展示它调用了搜索工具以及随后的爆炸半径查询。这表明如果智能体足够擅长使用工具,就能利用这些新工具构建对代码库的理解。接下来是一个互动环节,你可以自行构建所需的任何工具。基本工作原理是我们编写一个能执行任意 Cypher 查询的工具,例如查找指定文件路径中的所有符号并列出。
你甚至不需要了解 Cypher 语法。你可以给智能体一个包含模式信息的系统提示,告诉它不同的节点和边是什么。然后用自然语言提出请求,例如“我想查看实现某个特征的所有结构体及其对应的特征”。运行后,智能体会尝试理解并生成相应的 Cypher 查询来从图中提取信息。
虽然今天无法深入第二层,但我们可以略窥一二。到目前为止的一切都非常确定:你获得结构并获得解析。我之前提到的第三层是实际的语义含义,你可以让大语言模型对这些符号进行总结并嵌入向量。Typedef 实际上为此提供了一个名为 Fenic 的开源库,这是一个数据框库,用于结合传统的数据框操作与大语言模型。
在这里我们简单配置 Fenic 以连接 Open Router,并提取一些信息。我们将尝试对特定的 Rust 项进行分类,包括其类型、限定名称和文档字符串,然后用一句话简洁地询问“这个 Rust 项目是做什么的”。当我们运行 Fenic 时……
在这个语料库的这些行上,我们从大语言模型(LLM)获得汇总后的输出。从长远来看,思路是将这些信息整合到你的图中,以便智能体也能获取这些知识。这样做的好处在于生成过程是可审计的。如果你查看后发现对某个函数功能的解释是错误的,我们可以直接进行修正。如果智能体在运行时自行推断出的功能描述有误,你可能要等到它写出无法工作的代码时才会发现问题。
好的。这是第一个笔记本的内容。我们还有更多话题要讨论,但我很乐意回答关于已展示内容的任何问题,稍后我会重新上传相关内容。我看到后排有两位朋友想提问。请带上麦克风过来测试一下……好的,请问图是在运行时重新建立的吗?比如当你构建或遍历这些结构的时候。
请定义一下“运行时”。是指当拉取请求(PR)或 CI/CD 流程触发时吗?
是的,在理想世界中,我想我们并不是为了生存才开发这款产品。但在理想情况下,这些信息应该被缓存并以规范形式保存,比如基于主分支的“黄金版本”。当你本地调整或修改内容时,可能需要维护一个本地副本。理想情况下,这个副本应支持增量刷新,这样你就不必一开始就处理所有数据了。
我的问题与此类似。你认为维护这些结构更新的成本与性能及保真度的提升相比如何?因为至少对我来说,我是利用 AI 来进行大规模更改的。所以很难立刻判断是否值得去维护这些结构以保持更新。
确实如此,我认为关键在于很多这些问题可以归结为增量处理。因为你并不需要对未改变的内容重新建立索引。当你进行大范围代码库更改时,虽然修改了很多文件,但也有很多未变动的文件。如果我们能聪明地进行增量重索引,只跟踪变化部分并重新索引那一部分,目前的经验表明这种权衡是值得的。特别是当需要保持索引的上下文规模增长时,这样做变得更有用,因为每次实时处理的成本会变得更加高昂。非常感谢,这非常有用。
我想知道这在真实的、语言结构混乱的代码库中是如何工作的?例如,当存在动态分发或并非所有类型都在编译时(或者更准确地说,在分析代码前)可用时。你们尝试过吗?有哪些局限性或建议吗?
是的,我这里有一个第三笔记本专门讨论这一点。它主要讲解如何在像 Python 这样缺乏类似 Rust 那样易于访问类型图的语言中实现这一功能。我们可以使用其他工具,如 Griff 和 Same Type Sitter。Griff 可以提供静态解析以获取部分类型信息,你也可以使用 Tai 进行类型检查以增加索引精度。是的,随着不同语言的变化,难度确实呈指数级增加。我稍后会对此进行更多讨论。所以答案是肯定的,我也考虑过这个问题。这是一个更难的问题,这也是为什么我今天选择解决一个相对简单问题的原因。
我认为你不应该为了构建产品而这样做,这是明确的。[笑声] 这是一个用于教育目的的工作示例。人们经常构建这类代码图,我不会建议为了单纯的代码分析而从头开始构建一个。我不确定这是否回答了你的问题,我认为构建这类东西是有价值的。但我不认为每个人都值得自己去建一个。目前市场上已经有很多人在做这种代码层面的分析工作。所以我认为,如果目的是作为产品出售,个人构建可能不值得;但如果目的是为了学习如何构建智能体、工具以及上下文图,那么构建它是值得的。我认为这个问题的规模决定了它的实用性所在。这回答你的问题了吗?
太棒了。那么,关于你之前提到的数据上下文层……我假设这对开发者有用。即开发这个上下文层有助于生成更好的 SQL 过程等。我有两个问题:第一,你是否观察到它在运行时(即推理阶段)也能提供帮助?第二,你有任何基准测试结果来证明这一点吗?
当然。正如 Yonyi 在演讲中提到的,我们确实有相关的基准测试结果。我们试图找到具有代表性的数据架构案例,但这很难做到。因为你可以构建一个简单的教程示例,比如“我的智能体在我的 30 个模型语料库的测试中取得了 100% 的成功率”,但这并不令人印象深刻。因此,我们试图寻找更大的开源数据架构示例。最终我们选择了 Mattermost,这是一家即时通讯和聊天公司。他们甚至开源了整个 DBT 项目,包含数百个模型(大约三百到三百五十个)。我们能够下载这些数据,在其中模拟数据,然后针对内部特定场景运行评估。比如要求它执行特定任务、故意破坏某些部分并修复,或者构建新的一组新功能。通过建立评估层,衡量其正确性、完整性和效率等指标,我们发现拥有适当上下文时,智能体在整体及特定类别中的表现确实更好。
就像完成那些任务,比单纯运行 Cloud Code 更好。那么关于推理时间的那个问题呢?是的,你是指代理实际运行的时候吗?对,当代理真正运行时。我们正是在那时发现它最有价值,因为当代理运行时,它会查询我们要构建的所有工具,这才是真正发挥价值的地方。如果代理能够在运行时查询所有这些内容,它就不必一次性重新推导所有内容了。
那么,你们有没有在 NL2SQL 基准测试(如 Bird 或 Spider)中尝试过这个呢?是的。我意思是,既试过也没完全试过。我们在一些基准测试中尝试过。但棘手之处在于,产品的目标不一定是编写分析性 SQL 代码。我们要为构建的东西更多侧重于数据工程方面。
通常人们会说“我们将构建文本转 SQL”,每个人都在尝试这样做,但这可能不是重点。更重要的是说:“我们要构建工具,让你真正信任文本转 SQL 的结果”,而不是我们直接为你编写更好的分析查询。我们要确保当你进行日常的数据架构维护工作时,我们能够提供帮助。这样回答你的问题了吗?
好的,让我们回到幻灯片上。我们在最后看到的是,我们可以区分不同的发布列表。因为我们能够查询新的图上下文,并且能够通过一次跳转找出给定符号的所有依赖关系,而无需重新抓取。查询的工作原理是匹配完全限定的名称,然后找到所有依赖项,就是这样。
这是一个很棒的研讨会,我们关于 No. 的部分结束了。但这确实很棒,这个工具真的很强大。它能在一次调用中完成通常需要多次图操作才能做到的事情。即使你做了完美的抓取,我也认为无法在一次调用中完成这些工作。
但问题是代理必须实际使用它。当你把这个工具给代理,并把它与代理已有的所有现有工具一起提供时……我们有一个很酷的工具,看到了它做的惊人工作。但是如果你给它访问图工具和 Grip 的权限,它还是会运行 Grip。即使我们给了它惊人的工具、代码库的可遍历预计算模型,它仍然只是运行 Grip。
它会抓取并读取文件,运行 Grip,有时甚至会触碰图结构然后感到困惑,最后又回到等待处理图数据。这就是没人告诉你的地方。人们都说:“哦,他构建了这些很酷、性感的工具,它们能完成惊人的工作。”但代理必须实际使用它们。
你必须进行提示工程和其他艰苦的工作和评估,这才是最难的部分。你可以在一个下午构建笔记本、创建惊人的图上下文。但让代理真正使用它才是难点所在。为什么会这样呢?这是因为后训练模型被设计和训练为调用这些工具。
在它们的训练中,有无数情况下的答案是“使用 Grip 并读取”。你无法仅仅通过制作更好的工具来对抗这一点。你可以做出更好的工具,但代理的“肌肉记忆”不会让它使用它。这与人非常相似:你可以给人更好的工具,但如果强迫他们使用,他们会不高兴。
重要的是,你并不是在竞争你的工具是否比代理工具箱内的所有工具更好。你是在对抗它自身的反射机制。既然毫无疑问,如果它能查询图结构,效率会更高、效果更好(我们稍后会证明这一点),那么我们如何让它做到呢?
答案不是与模型的先验经验对抗,而是顺应它。让图结构感觉更像模型在使用 Grip 的方式。你可以把一个可能叫作“查询搜索”或“图查询”的工具,直接命名为“搜索”。代理会以某种特定方式使用它。
我发现如果你将工具命名为“搜索”,在没有训练的情况下,代理基本上总是会对它进行多关键词搜索。所以你必须与模型各退一步:你给工具命名为“搜索”,考虑到模型将如何查询它,然后从图结构中获取结果。
并以与 Grip 相同的形式返回这些结果。你采用相同的工具结构和响应格式,并将其返回给模型,因为这是模型被训练处理的方式。它知道如何处理这些东西。你无法仅仅通过提示就让模型完全改变其自身行为。
最后一件事是,你的工具必须在第一次调用时就产生回报,每次都要如此。因为当代理走上一条路并发现死胡同时……比如它调用搜索但没有得到任何结果。有时它会认为搜索坏了:“如果我没有得到结果,而应该有结果的话,也许搜索坏了?我应该尝试另一个工具。”它不会回去再次使用那个工具。
如果它确实回去了,可能会试图用不同的方式使用。你无法预测这一点。所以你必须尽力确保你的工具立即返回非常有用的内容。否则,它们只会回到 Grip 并且永远不会再回来。
第二个笔记本更短一些。这是一个稍微聚焦一点的例子,全部关于评估(Evals)。你如何实际证明代理正在做你认为它应该做的事情?以及如何真正证明你的……
工具是否优于可能已存在的现有工具体系,对吧?因此,所有这些内容都已经预缓存了。我进行了这次运行。如果您确实想在本地运行笔记本,您绝对可以将样本大小设置为大于零的数值并实时运行评估。但这会花费很长时间。别怪我没提前警告你。不过,我确实在 overnight 期间运行了这次测试,使用了五次重试和电子邮件集合中的三十个问题。这就是这个笔记本中这些结果的来源。
因此,在完成设置后我们要做的第一件事是建立测试用例。我们将拥有一个配备新图工具的智能体(Agent),赋予它访问相同模型和我们图工具的权限。同时,我们将拥有一个作为对照的智能体,它也拥有对同一模型的访问权以及由 Pydantic 维护的一套文件系统探测工具。这就是我们的两个测试智能体,随后我们将构建一个由问题组成的语料库。
每个问题都代表一项真实任务。我们要确定诸如“如果我更改发布列表(posting list),哪些公共类型会被破坏?”之类的问题。这里有两个发布列表,我该使用哪一个?诸如此类的事情。我们试图说明哪些问题对图结构特别有利,哪些可能对通用语言模型(LLM)更好。当然也有一类控制性问题,它们对两者都有利。
对于黄金数据(Gold Data),我们实际上是从图结构中提取元数据,并将其反向交叉引用回源代码。因此,并不是我们用图的输出来污染黄金数据的上下文,我们必须进行交叉引用来确保黄金数据没有作弊。现在我们可以加载这些黄金数据,并基本上开始建立实际的评估结构本身。
这展示了什么是好的评估问题与差的评估问题的区别。理想情况下,您的评估应该能够被确定性地评分。我认为使用语言模型作为裁判(LLM as a Judge)在许多用例中非常棒,但当您应尽可能努力使评分具有确定性时,它往往被过度使用。这将减少运行中的方差和成本,并在长期内让您更满意。
因此,您希望创建那些答案可以从黄金数据中提取的问题。您需要能够构建一个有效的黄金答案结构,并能够将智能体的回答与黄金数据进行比对验证。您不应该只是询问智能体提供其意见,然后要求另一个智能体用其意见来反驳该意见。这类问题通常无法取得良好的评估效果。
好吧,这是一些示例文本,您无需过多担心这部分内容。这基本上是在构建我之前提到的测试用例。这是确定性部分,我们检查它是否引用了我们期望它引用的所有证据。基本上,我们正在创建一个示例列表、一个它应该引用的内容列表,然后检查它是否引用了所有这些内容。
对于评估本身,我们使用 Pydantic Evals 中的配置,因为它设置起来非常简单。其工作原理是构建出我们的两个不同智能体,然后构建代理运行器(Agent Runner)本身,最后构建评估器。这实际上将根据我们面前的给定任务来评估智能体。
我们将基本上比较智能体的陈述与我们对每个给定任务的期望,从而能够对比预备智能体和图智能体之间的响应和结果。这样我们就能确定:第一,我们的技能的实际效用是什么;第二,随着时间的推移我们可以改进什么或做得更好。
请继续运行评估。我不会再次实际运行评估,因为那大约需要三十分钟。但我们可以看到第一次运行时使用了 K=1 的参数。也就是说,对于数据集中的每个任务,我们只运行一次图智能体。结果显示该智能体的表现并不好。您可以看到另一个智能体稍微领先于它,但这并不一致,也不是我们想要的结果。
但我们需要理解的是,目标从来不是构建一个比 Graph 更正确的东西。因为我们这些日常使用 Graph 和编码智能体的人都知道,它在自己的领域非常擅长。它之所以使用 LLM 是有原因的,很多时候它是不会出错的。重要的不是它在某一次尝试中是对的,而是当您连续十次、十五次或二十次尝试时,它每次都能做对。这就是我们需要开始对这些测试进行采样的方式。
现在当我们说在同一语料库上运行这组相同的测试五次以获得 Pass@K 指标时,我们会得到稍微不同的答案。我们有两个统计数据:Pass@K(是否在任何一次运行中正确)和 Pass@1-K(是否在每次运行中都做对)。Pass@K 显示在这五次运行中,它们非常相似。实际上 Graph 在这方面略有优势。
Graph 不会采样任何私有变量,只采样 crates(Rust中的包/库)的公共部分。因此有一些控制性问题 Graph 总是做得更好。但有趣的是,当改变指标为“是否在每次运行中都做对”时,数字完全反转了。突然间,图智能体在连续五次运行中每次都做对的能力是 Graph 的两倍。
我们可以看到,在执行这些操作的框架内,它不仅做得更一致,而且效率也更高。在我们所有不同运行的过程中,平均而言,图方法使用的 token 数量少了约三倍(3.1倍)。最后一部分是:我们如何实际让智能体使用图?这正是评估发挥作用的地方,因为您基本上需要编写一个评估脚本。
该脚本会说:“检查我轨迹中的所有工具。我的智能体使用我希望它使用的工具的频率是多少?”然后您可以获得某种采用率数据(adoption numbers)。这样您就可以达到这样一个境地:您可以说,“哦,我写了一个提示词,其中没有任何指示要求使用工具。”
工具使用率达到了34%。我回去修改了我的提示词,现在工具使用率变成了72%。”对吧?因此,评估是你唯一能够真正理解智能体是否使用了你希望它使用的工具的方法。这只是一个针对该概念的测试,我们有两个不同的提示词,都试图用相同的工具集回答同一个问题。如果你稍微引导一下这个提示词——如果处理得比较粗糙,它就会直接抓取数据,然后开始做其他事情。而在理想情况下,如果你引导得当,它会去执行搜索并使用“获取符号”功能来找出实际答案。
构建扎实、诚实的评估方案的配方是从你的语料库中挖掘真实的任务候选项。尽可能构建确定性的、基于锚点的评分器,并首先对评分器本身进行单元测试。我喜欢Pantic评估,虽然市面上有许多其他评估工具,但它们的核心结构都是相同的:你拥有数据集、案例和评估器。确保多次运行你的评估,并报告K次通过率(pass at K)、K次准确率等所有不同的指标。
同时,确保在评估中加入那些智能体可能不擅长的例子,因为你并不希望它达到100%的通过率。如果你的智能体能100%通过你的邮件测试,那就没有意义了,因为你的邮件太简单了。智能体应该在处理邮件时接近失败,就像“真金不怕火炼”一样,这是观察智能体在尽可能困难的情况下表现如何的机会。因此,仅仅提供非常简单和简单的评估对任何人都没有好处。
关于这个评估笔记本,在我跳回幻灯片进行演讲的最后部分之前,大家有什么问题吗?我刚才在想那个例子中,当你讨论智能体经常使用GP而不是Graph时。所以你不得不重新调整它成为一个搜索工具。有没有尝试过将Graph提炼成一个文件结构,比如以某种损失为代价的文件结构,然后让GP自然地在其上搜索,这样你就不需要定义任何单独的工具了?
“是的。今天还没有这么做,主要是因为时间紧迫,但我认为这绝对是一种合理的方法。我的意思是,你知道企业LLM维基(wiki)中有很多不同的东西,你可以从Markdown语料库中获得很多这些好处。我认为这确实是真实的,并且你会从中获得类似的益处。但是在Markdown中,比如Ele Wiki这类东西,我认为你的问题仍然在于你想要真实的边和溯源关系。这些连接之处是最难处理的,因为即使你做了类似Grain的事情……”
“是的,它在Markdown中构建了所有内容,但它确实在此基础上构建了一个图,让你看到时间线和连接等内容。所以我认为在某种程度上,如果你的任务需要理解这些不同事物之间的关系,那么在某个层面上你将需要某种形式的图。尽管我认为通过类似的方法也能获得非常相似的成功水平。”
“你认为在这种设置下,对于让非技术、非数据工程师背景的人以此作为代码库中的指南来发起PR(拉取请求),还有什么缺失的吗?你尝试过吗,或者在赋予人们这种方式之前你有什么要添加的?”“哦,像这样作为一个工具?是的。我认为这是一个好问题。”
“是的。我认为最容易的部分可能是开始将其作为MCP(模型上下文协议)连接到你正在使用的工具中。这样你就有了这个代码图,除了你的云端代码外,你可以要求云端代码调用该图并执行其他操作,然后编写技能以便它正确地使用这些内容。我认为这是最好的方法。”
“但是我想是的,如果你技术背景较少或非技术人员使用,我认为目标应该是:如果你想让智能体在实际编码的上下文中使用这些东西,你会将我在这里讨论的这些提示词工程内容提升为技能,并交给你的编码智能体。”
“是的。就像这个PR第一次落地给工程师时,由于受到这种更确定性的引导,结果是一个半成品的初稿。”“是的。而且我认为确定性部分是有帮助的,因为我们之前讨论过后面那个问题:你可以增量地查看差异(diff),对吧?确定性地分析它,并快速得出结论关于发生了什么以及自上次以来有什么变化,而不是等待智能体去阅读所有内容、查看所有更改并给你返回相同的差异。”
“那里有一个问题。我想……你有没有实验或思考过不同级别的粒度?我在一些早期的演讲中想到的一件事是,你进行下一个词预测,并且会有前缀。人们说这就像……你知道,所有东西都在代码库中整个字符串的层面上;这里有符号,但符号有一定的粒度。在代码库中存在更高粒度的层级,使这些东西可搜索和可供智能体访问可能会有回报。我想知道你是否考虑过这一点,或者你对如何接近这个问题有什么想法?”
“这是一个很棒的问题。我确实思考过这个。实际上,我将在演讲的最后部分讨论它。这里肯定有一些后续工作要做。如果你真的试图构建这个系统,你会使用类似SCIP的工具来实际追踪代码库内部的调用图。你可以看到什么在调用什么,以及它们如何交互,因为你可以追踪符号及其在代码库中的移动方式。但如果你想实际跟踪什么在调用什么以及它们是如何连接的,那就是你所说的高层级,你需要……”
需要一种稍微增强型的方法,即确定性的解析层级。你可以使用 Rust 或 SEIP 来实现这一点。对于 TypeScript 或 Python,也有类似的 SEP(结构化提取产物)输出。我本来打算把这些都做了,但我时间不够了。(笑声)但回答你的问题:是的,我们肯定可以从给定的代码库中提取更多的结构信息。这正是我们在 Typedef 所尝试做的——寻找所有不同的方法,以便从我们拥有的数据中提取尽可能多的上下文。
我有一个小问题:你如何描述“数据上下文层”这一概念与类似“数据目录(data catalog)”这样的东西相结合?是数据目录有助于为图提供输入,还是两者在企业环境中有些冗余?
我认为它们并不冗余。我认为它们回答了不同的问题。你的数据目录是对你数据仓库中确切存在的内容的一种固定理解,我认为这与上下文图试图提取内容的要求是不同的。我认为它们可以共存:你从目录中读取信息以构建上下文图。所以,你们会尝试这样做:基本上是说,“好的,我有一个 dbt 项目,其中包含许多模型”,这些模型在 Snowflake 中构建了表和视图。
因此,我们可能会说我们要摄入这个 DBT 项目。我们将拉取所有的 SQL,解析它们,将所有内容链接起来,然后我们会说:“哦,好的,这个模型对应于这张表。”让我去查询目录,获取关于该表的信息,并将其也拉入图中。这样我们就可以说:“好的,这个模型正在生成这张物理表,而这张物理表有这些列,这些列有这些配置文件(profiles)。”
这些信息都来自目录。所以,关于信息、模式架构以及配置分析的所有这些 essential 内容都在目录中。因此,我会说它们是共存关系,而不是替代关系。(清嗓子)让我们回到数据工程以及“评估(evals)”这一概念。你是否以同样的方式思考,即提出一组数据问题,然后观察使用工具与不使用工具时的反应?
是的。 对,没错。 precisely(正是如此)。哦,你还有更多吗?抱歉。 对不起。 哦,我不知道你是否说完了。嗯,绝对是的。
正如我之前所说,我们确实使用……基本上试图找到尽可能真实的数据语料库,然后尝试寻找多种不同的场景。例如,你有一个基本的“修复 bug”的场景:我们在代码库中注入一个引入 bug 的差异(diff),然后要求代理去尝试修复这个 bug。你可以测试它是否查看了正确的位置,检查了正确的内容,以及在修复 bug 时没有通过“打补丁”的方式在上游或下游进行错误的修复。
它实际上找到了正确的模型并解决了正确的问题。你可以通过改变提示语来使这些测试更容易或更难,比如:“哦,我认为模型 XYZ 有问题。”或者对于更难的例子,“我的仪表板 Y 出现了这个问题,这个数值翻倍了。我不知道为什么。”这迫使代理从依赖关系向后工作,沿着树向上追溯,以了解实际根本原因来自哪里。
然后断言它确实进去并解决了这个问题,而不是置之不理。这就是难点所在。问题是代理总是倾向于作弊(寻找捷径)。因此,在你的评估框架中,你必须非常小心地加固它。例如,如果你的代理说:“哦,我的测试用例是……”它会签出一个分支,然后尝试修复问题。代理会非常乐意作弊,使用 git 回到 main 分支,试图弄清楚你是如何在这个分支中引入问题的。
它会非常乐意地找到名为“这是一个测试”的分支,心想:“哦,这是个测试。我可以作弊。”因此,你必须非常小心地加固你的框架,使其真正代表你希望代理执行的任务。因为那里始终存在这种危险。(笑声)
对于这些评估,你觉得人们大多是从头开始编写它们吗?还是有一些现有的框架可以使用? 确实存在一些用于此目的的现有框架。ADE bench 就是一个很好的例子,它可能是目前此类数据工程基准测试的行业标准。它们采用非常相似的方法:使用差异在仓库中创建问题,然后让代理去读取数据仓库中的数据以及代码中的内容并尝试解决问题。
所以这些确实存在,人们正在将它们连接起来使用,我认为这正变得越来越普遍。但我认为更难的部分始终是围绕它的脚手架(scaffolding)。对于代码,你只有你的代码库;但对于数据工程,你需要一个正在运行的数据库。你需要它用正确的错误数据进行填充,并且这些数据在内部必须是一致的。
所以你不希望代理转来转去试图弄清楚:“哦,为什么 2024 年的数字不对?我们只在样本集中生成了 2024 年的数据。”因此,我认为人们经常遇到这类问题。这就是为什么人们在尝试在 ADE(可能是指 AD Bench 或类似标准)中标准化它。我认为困难的部分是构建一个其他人可以使用的基准测试,并且该基准测试拥有足够大的数据集。
那将是棘手的部分,而这尚未有人做到(至少对我而言)。嗯,是的,你知道的……我们回到这里:这仅仅是刚才看到结果的总结。我们看到,随着能力的增加,当我们告诉代理更多地使用工具时,它在回答同一组问题时的能力变得更加一致。
并且能够更高效地回答这一组相同的问题。举例说明为什么多次运行这些评估非常重要:这不仅仅是关于单次准确率,而是关于确定性(determinism)。真正重要的数字是:你如何确保你的代理实际上在使用新的工具?因此,你真的要确保提高采用率数字,并测量工具的采用情况。
因为这才是真正能让你走完最后一段路的关键。你知道的,一个优雅的工具如果只有三分之一的时间被使用,那它只是三分之一的工具。(笑声)
更好的工具受限于这个指标。因此,我们构建评估的目的不是为了证明工具很好用,而是为了证明它没有被闲置使用。这场演讲最难的部分基本上在于承认图(Graph)确实非常有效。我做了大量工作,构建了上下文图谱并运行了评估,结果却是平局。这让人感到沮丧,因为你本能地想要继续优化、构建该工具并从中挖掘更多价值,但这其实是一个陷阱。这种“构建更多内容”的冲动有时会导致沉没成本谬误。智能体有自己的反射机制,我认为它应该这样做,而我也认为我应该那样做。我们试图在中间找到平衡点,并真正找出前进的方法。
人们可能会想:“也许你只需要更多的测试、评估或某种改进。”最终,你会产生一种错觉,认为只要增加严谨性和测试次数,智能体就能更好地使用工具。但也许你应该问的是:“为什么图(GP)能如此有效?”而不是“为什么不使用我的工具?”也许答案是,图本身已经运作得相当好了。因此,与其与之对抗,不如稍微顺应它一点。既然智能体无论如何都会使用图(GP),那么如果你不断试图在工程上超越它,只会让你感到沮丧。
因此,如果可能的话,顺应其特性进行设计,帮助智能体去做它们本能想做的事,这将极大地帮助你。我现在做过多次演讲,每次结论部分我都会说:“让我重新运行一组评估。”结果我不得不重做整个演讲,因为结论完全相反。原因是我编写的评估代码和构建的测试框架有误。虽然得到的答案在方向上是正确的——显示图优于GP,但我深入挖掘后发现:我们制作的工具返回的是文件路径,但不是正确的路径。
有时当智能体调用搜索时,它会找到错误的路径并沿着错误的方向尝试读取文件。因此,你必须深入阅读所有的追踪记录(traces)。进入 Logfire 或你发送追踪数据的任何地方,确保你真的在阅读它们。因为如果你只看记分牌,你可能会得到看似方向正确或表现良好的答案,但这些答案可能掩盖了底层更严重的问题。
这些潜在问题实际上在你的架构内部要大得多、棘手得多。因此,解决方案从来不是简单地提高或美化某个数字,而是构建更严格的评估。阅读追踪记录,找到缺陷(artifact),加固测试框架,然后重新运行。永远不要相信你自己的记分牌。以下是我的一些其他想法:模型正在变得更好,我们可以继续相信模型的方差会减小,并更倾向于执行我们希望它们完成的工作。
因此,构建上下文和工具可能是相对容易的部分。真正的难点在于建立评估循环、监控可观测性,并确保智能体确实使用你希望的工具执行了你想要的工作。所有相关代码、评分和循环都在仓库中,欢迎大家在空闲时间自行探索。之前我提到过,我在 Rust 上走了捷径。Rust 编译器已经运行过一次了,这算是作业的一部分。
我选择了免费的语言版本,因为 lesson 是:你可以在相对简单的问题实例上构建出工程精良的解决方案。通常来说,如果能在稍容易的问题上让事情跑通,再将其扩展到更大规模的问题时会更容易一些。因此,我们现在可以说:“哇,我们为 Rust 做了这件非常简单的事,现在我想为 Python 做同样的事。”但 Python 更难。
如果你看这些代码行,如果不使用静态分析等工具来加载这些信息,你是无法取得进展的。如果你直接让 GP 客户端处理,你会陷入拥有五万个客户端的死胡同中,遇到很多困难。因此,在这种情况下,代码图谱(Code Graph)真正发挥了作用。当代码库更大且语言结构不那么规范时,代码图谱比图(Graph)更加有效。
对于 Rust 来说,我想查看实现了某个特征的内容很简单:搜索“impl”即可。但对于 Python 来说这就困难得多,因此你需要构建更稳健的东西。不过,图谱的结构和工具的结构不一定需要改变;我们需要改变的是数据摄入和加载的方式。如果使其模块化,你就可以为任意数量的语言或项目构建它。
这就是 TypeDB 的工作原理:我们试图建立尽可能多的不同摄入过程,从许多地方提取数据并将其综合为一个整体。这里还有另一个笔记本,由于时间关系我不再详细讲解,但欢迎大家自行探索以了解更多细节。最后一点是关于“意义层”的讨论。结构告诉你什么连接着什么,而意义则解释了为什么这样连接。
代码中的注释、文档字符串以及其他信息都包含了这些内容。整合这些信息有助于我们理解程序员编写代码时的意图,而不仅仅是当前存在的代码结构本身。因此,我们需要添加类似的小处理步骤来总结代码信息,并在逐行的基础上理解它,以便搜索不仅能回答“代码在哪里”,还能回答“代码做什么”。
当你阅读这些追踪记录时,重要的是要确保你不仅看表面数据。
构建类似的东西时,你实际上是在建立一种可审计的语义分析层,它将叠加在确定性分析之上。关键部分在于提出许多小问题,而不是一个大问题。答案并不是写一个非常……好的提示词然后就把 SQL 扔给它说:“嘿,这里有一个包含十个字段的严谨模型,请从 SQL 中提取你能得到的所有信息。”这行不通的。
模型会产生幻觉,它会编造信息,无法收集尽可能多的数据。相反,你不应一次性让模型询问代码块中的十个问题,而应针对代码中不同的较小部分提出十个不同的小问题,以回答特定的疑问。你要对结构进行确定性解析,然后提出许多聚焦的问题来发挥模型的优势。
与其让模型查看整个 SQL 代码块并告诉你所有列的含义,不如解析 SQL 后说:“嘿,这里有一列。这是它的类型、注释。”它是度量值还是维度?此时可以让代理进行分类和总结,这正是它擅长的。如果你想赋予其更多权重,就给它提供更多针对该单一焦点问题的提示输入,而不是塞入更多问题。
你提供的是它能回答那个单一问题所需的上下文。这就是我们库 Fenic 的用途,也是我们在 Typed 中使用的部分。最后一步,即梯子上的最后一阶,意味着我们已经将所有这些分层堆叠起来,知道它们如何关联。现在,它到底是什么意思?我们可以开始在最后一层使用 LLM 来提取语义理解。
正如我之前所说,这里还有很多可以做的。你可以构建一个代码库图分析代理,利用调用链来理解谁在调用它、谁引用了它。它可以读取代码内外的注释,并整合来自外部的上下文,比如 GitHub 问题和 Linear 工单。这样构建出的东西才更有用且更可靠:从小处着手,从代码库中提取结构。
然后在此基础上叠加语义理解,这对于构建对代理有用的基础架构至关重要,使其能在此空间内工作并做出更改。这就是最终目标。非常感谢大家,这是我的演讲。我是 Typed 的 Brandon Calendar,这是研讨会链接和我的 LinkedIn。谢谢,我可以回答任何问题或讨论其他话题。
有人尝试过跨仓库的这种工作流吗?比如在微服务之间或通过 HTTP 边界,有成功吗?嗯,不太算。我认为这主要适用于单个仓库。我们在大型数据架构产品中大规模这样做过,所以我觉得可行。难点在于确定不同部分之间的边界。你需要编写边界分类器来理解……
比如我尝试用 Python 和 Rust(通过 maturin)做例子,你可以构建类似的场景。你需要一种方法来理解当 Rust 调用 Python 代码或反之时的情况,以建立它们之间的链接层。是的,你可以做到,但这需要更多工作。还有其他问题吗?抱歉。
Cursor 做了很多第二层的工作。当你看到 Cursor 在代码库中搜索时,通常是在进行语义搜索或向量嵌入搜索。这确实有效,这也是为什么有这么多代码 RAG 产品专注于语义检索的原因。这种方法能让你找到所有想要让代理检查的代码位置,但你仍需逐一检查这些位置。
我的目标是:如果我想一次性说“我知道我正在看这个函数”,我希望代理无需抓取所有位置,就能通过 IDE 点击查看其调用点。明白吗?还有其他问题吗?好,这边一位。
我想象构建这部分的一大乐趣是最终得到一个可以检索的图向量?是的,我对缓存和存储很感兴趣。随着扩展规模,确保代理首次调用就正确至关重要。是的,当你开始扩展图数据库时,会发现情况很快变得复杂。你完全正确。
我认为存储不是主要担忧,虽然图确实容易蔓延且难以垃圾回收,但我更担心运行跨不同部分的图分析所需的计算量。因此扩展时,重点应放在增量性上:如何在不重新生成所有内容的前提下重建它,这可能最重要。
这是我们正在 Typed Death 努力解决的问题,试图在更大的规模上构建它。因为你不希望反复去查询你的数据仓库,比如500次;也不希望你重新编译所有的SQL代码。你希望能够在数据流入时即时更新内容。因此,我认为绕过这一问题的最重要方法是首先将增量索引设计为第一等公民。这几乎是唯一能让你达到处理其他扩展性问题的途径。我认为,在应对如何存储或计算数据等实际规模问题之前,你必须先解决信息同步的问题。
是的。对我而言,构建和维护黄金数据集始终是最大的难题。我完全同意你的观点,这确实是我没有完美答案的问题之一。实际上我有两个回答:首先,“黄金数据集”并非绝对必要。如果我在构建并测试系统,我会选择代码库的一个特定快照作为基准。这样我就可以明确这是我的快照版本,当我运行评估时,拥有一组确定性的黄金问题。这听起来像是在回避你的问题,但如果是实际设计系统,我会针对快照进行构建。
这样我的评估问题是确定性的,不会随时间改变。因为在理想世界中,你的智能体应该能越来越擅长回答相同的问题。但基于不断变化的语料库来改变问题,并不能真正帮助你评估智能体是否变得更好。评估智能体进步的最佳方式是让它们重复执行完全相同的任务。因此,对于评估而言,黄金数据集应针对固定语料库保持恒定。这说得通吗?
现在如果你是在进行实时测试,确实需要找到新的方法来针对代码库进行测试。但我认为这与固定的评估集是两回事。这说得通吗?是的,但是……如果我们要以代码库为例,假设有一个复杂的错误:一个函数调用了另一个包含错误的函数。你设置了一个评估来测试如果引入该错误,智能体是否会定位到相关函数并修复它。但随后你进行了重构,那个函数实际上不再被使用了,导致评估失效。
我明白你的意思了。在这种情况下,我认为我的方法依然适用。当我构建评估时,我会将其定义为特定代码提交版本的组合:即我当前的代码状态、我希望引入以测试智能体修复能力的具体差异,以及由此产生的更改结果集。这些应该始终保持恒定。我理解你的担忧:代码确实会变化,可能不再需要修复该问题,但这并非你测试的重点。
你并不是在测试智能体对新代码的反应能力,而是在测试它在编写评估时所处的特定情境下做出更改的能力。即使该任务不再相关,它仍然在测试某些有用的东西。这说得通吗?精彩的演讲,我很喜欢。这让我想起了本体论中的类概念,而 Palunteer 正是靠此大获成功。是的。那么一旦构建了本体和依赖关系,我们能否用这些高层结构与大型语言模型对话?是的。使用这些类吗?是的。这是否基本上是你在此时暗示的内容?
是的,那基本就是我们的产品理念:我们构建上下文图,并在其上建立本体层。我们试图围绕你的数据架构组织所有信息:事物指向何处、位于何地以及含义是什么。然后我们赋予智能体访问这些工具集的权限,使其能够回答关于你数据架构的问题并进行更改。抱歉打断一下。
好吧,这可能有点天真,但回到你们正在构建的产品以及此处展示的内容来看:这里展示的许多内容与 Neo4j 在早期工作坊中演示的非常相似。即,这是一个知识图谱;作为拥有领域知识的开发者,你将在该区域构建一个知识图谱,然后提供给智能体,让智能体查询数据库以获取一些语义信息。
我负责管理一种错综复杂的数据状态,其中包含大量第三方软件即服务产品、内部系统以及其他组件。我的团队负责将这些信息整合进来,其中一些高度可配置(如 Salesforce)。你将信息导入数据仓库等系统,最终在另一端实现商业智能和交互。因此目标是如何让我这个由优秀人员组成的小型团队,一次性、通过良好的版本控制定义所有这些内容之间的语义关系。
这样我就可以为人工智能智能体提供上下文和工具,主要用于分析工作,偶尔也用于回写数据。同时为必须使用这些系统的人类提供文档(无论是入职培训还是其他用途)。目前我尚未找到最佳实现方式。我们要聊聊吗?那么,你会给我提供这个方案吗?因为你刚才说你们不会做数据目录……是的。
不,我们不做(仅限于此)。我的意思是,我们将不限于数据目录,因为我们不会只读取你的 Snowflake 目录或仓库信息。我们的目标是从 Salesforce、Fiverr、Airbyte 等各种来源,以及所有传输机制中读取数据。
你拥有 Airflow 等工具,我们需要将所有这些整合到一个统一的理解上下文树或上下文中。我们将构建一个我们可以实际遍历的上下文图,然后从中提炼出你提到的那些内容。语义层的主要问题是没人愿意去维护它,对吧?这几乎成了一个贬义词,因为一旦你构建好一个语义层,起初确实很酷、很棒且极具吸引力。但随后你必须每次添加新模型时都去更新它,这简直是一场噩梦。
因此,Typedef 的目标是让你能够真正跨数据架构中的不同层级进行推理。基于一个单一的图,你可以实际追踪数据如何从 Salesforce 一直流向你的 BI(商业智能)层。当代理问你诸如“应收账款是从哪里来的”这类问题时,它就能回答:“好的,这实际上起源于 Stripe 数据。”
然后这些数据会与下方的 Salesforce 潜在客户数据进行关联。为了弄清楚下方那个实际计算数据的环节,这种数据血缘关系非常有用。对我们而言,这不仅对负责开发的数据团队很有帮助,我还希望最终能让业务部门也能访问这些信息。
这样当他们查看报告并发现数据不对时,就不会直接来找我们团队发火说“这到底是怎么回事”。相反,他们可以指出:“哦,实际上这部分由这个系统中的这些人负责,这条记录属于他们。”所以我们需要把这些信息保持在一起。我完全同意你的观点。
我认为 LLM(大语言模型)在某种程度上让我们能够做到以前不可能的事,那就是充当业务人员、会计师、分析师与数据分析师或工程师之间的翻译层。我们想说的是,当业务分析师指出收入仪表板中的某个数字错误时,他们修复该问题的流程通常是提交工单。
这个工单会被放入某个系统中等待处理,有人接手、分类并试图确定谁负责该部分。然后将其转交给相关人员,他们又需要与原始负责人沟通。这简直太荒谬了,我在 Salesforce 工作过,所以我对这种高层级的流程深有体会。
Typedef 的目标是让业务分析师能够直接介入并说:“嘿,我的仪表板坏了。”他们可以输入仪表板的名称。我们可以在他们所在的会话中调出该仪表板,追溯并指出:“我想我看到了问题所在。可能是在多层之后发生了这种情况。”
我们可以检测到拥有该模型的是哪个团队,并直接为你提交工单。由于我们已经看到了根本原因——比如回溯三层发现某个选择语句是错误的,或者追踪到上周上线的一个变更导致了此处的问题。
我们还可以看到发生这种情况的时间点,因为有一个 Airflow 运行任务实际上填充了该数据。我们试图实现的愿景是统一理解从运行时到血缘关系的所有运作方式,让代理能够回答此类问题。
通过在不同层级之间架起桥梁,使这一过程更加高效。如果工单不再在五个不同团队间来回流转,而是立即被理解并路由给正确的团队。在那个理想的世界里,如果我们的产品无处不在且运作完美,该团队甚至可以直接说:“这个解决方案听起来不错。”
然后他们就会去实施它。这就是我们的梦想,也是我们要努力的方向,而且我们确实已经走在了正确的道路上。很高兴能进行交流。好的,还有最后的问题吗?没问题。如果你想在 LinkedIn 上与我们联系或随时交流,我们一直都在。
我们喜欢讨论上下文图这一主题。非常感谢大家的参与,我们稍后再见。谢谢大家。