LLM 架构设计原则:语言接口
语言 接口 是一种使用自然语言作为领域特定语言(DSL)或与系统进行交互的接口。它通过解析、处理和分析自然语言,以指导系统的设计、开发和执行。它的设计目的是提高开发效率、准确性和用户体验,使开发人员能够使用自然语言描述系统需求、执行任务并获取系统生成的结果。
随着越来越多的团队加入到这场竞赛里,我们会发现:基于大语言模型的软件架构与过去的不同之处,诸如于:我们需要新一代的 API。 我暂时将这一代 API 称为:语言接口/语言 API(大概会有大佬来发起新的名称),原因是:自然语言只是人机的接口,DSL 是 AI 与机器的接口、机器与机器的接口。
所以,在这篇文章里,我将继续总结一些内外部看到的经验,以及构建 ArchGuard Co-mate 架构时的一些思考。
模式:自然语言即 DSL
意图:使用自然语言作为领域特定语言(DSL)来描述系统的需求和期望,通过正则表达式等工具从自然语言中提取关键信息,以指导系统的设计和开发。
适用场景:以自然语言作为沟通媒介
示例:在我们习惯了大模型的能力之后,也开始接受了自然语言作为一种 API。与我们一般熟悉的 API 相比,自然语言作为 API 应该称为 DSL(Domain-Specific Language,领域特定语言)。
举一个简单的例子,诸如于我们前面提到的用户故事,其标准形式是:作为一个<用户角色>, 我想要<完成活动>, 以便于<实现价值>
。随后,我们就可以对其运行特殊处理(诸如于高亮):
作为一个购买商品的用户, 我希望能够退货, 以便于在不满意或商品存在问题的情况下获得处理。
随后,我们可以让 LLM(大语言模型) 生成正则表达式,以校验返回结果是否符合要求:
- 用户角色:
购买了商品的用户
- 完成活动:
退货
- 实现价值:
在不满意或商品存在问题的情况下获得处理
并作为下一代的输入,如将一句话的需求,编写成更详细的 AC(Acceptance Criteria,验收条件)。这种方式可以使用自然语言来描述系统需求,从而提高开发效率和准确性。
模式:实时文本流 DSL
意图:通过逐步、流式的方式返回结果,提高大语言模型的用户体验。
适用场景:当需要大量处理数据的任务时,一次性返回所有结果会非常耗时,影响用户体验。此时可以采用流式返回结果的方式,即模型逐步返回结果,让用户可以及时查看部分结果。
示例:在需要大语言模型返回复杂的结果时,如果我们期待它一次性返回所有的结果时,它总是非常的缓慢。在这时,采用传统的 JSON、Yaml 等格式,必然非常缓慢的,对于用户的体验非常差 —— 一个返回结果平均要几十秒。而除了在模型侧提升性能、本地减少请求,还可以构建能处理流式返回的 DSL。
这也是为什么我们觉得传统的 JSON 无法满足的原因,在返回正常的结果前可能解析失败,并且你返回的 JSON 可能不是完整的。
在这时,我们需要服务端支持处理 streaming response,并需要前端来处理。如在商业画布的场景下,就可以根据不同的一级 LIST 处理结果,并实时呈现给用户:
- 客户细分
- 目标市场:企业客户
- 客户类别:中小型企业,初创企业,创业公司,大型企业
- 客户需求:专业技术服务,包括但不限于IT咨询,软件开发,网络安全,数据分析等
- 价值主张
...
除此,根据不同的场景,我们可以返回不同的格式,如 markdown 表格、CSV、JSONP 等也能作为返回格式。
模式:DSL 引导的功能生成
意图:使用DSL和LLM结合的方式,以生成具有更好逻辑性和准确性的文本。
适用场景:适用于需要根据规范和约束生成具有逻辑性和准确性的大量文本的各种场景,例如软件开发、自然语言处理、数据分析和教育培训等领域。
示例:如我们所知,大模型(LLM)在生成文本时随机性太高,充满大量的不确定性。而由于,LLM 具备很好的逻辑推理能力,因此我们结合了 DSL 与 LLM 的强项,让它来编排 DSL 中的功能。
所以,在 Co-mate 里,我们设计了一套 DSL,以让 LLM 根据不同的场景填空和编写 DSL,如根据基础的规范来生成 DSL:
foundation {
layered {
layer("application") {
pattern(".*\\.application") { name shouldBe endWiths("DTO", "Request", "Response") }
}
...
dependency {
"application" dependedOn "domain"
...
}
}
}
在这里的 layered 是指系统的分层架构,layer 则是分层的定义,dependency 则定义分层间的依赖关系。最后,交由我们的系统来处理这个 DSL。
DSL 引导的功能生成是一种结合了领域特定语言(DSL)和大模型语言模型(LLM)的文本生成模式,通过 DSL 提供的规范和约束,以增强 LLM 生成文本的逻辑性和准确性。
模式:语言显式化重试
意图:提高大语言模型生成结果的准确性,并保持历史消息,以便进行对话记录和可视化。
适用场景:由于大语言模型的不确定性,生成的结果往往不够准确。此时可以采用显式化重试的方式,通过告知模型结果错误并让其重新生成,从而提高结果准确性。
示例:众所周知,由 LLM 生成的有确定性要求的结果,总会出现一定的错误。诸如于,我在使用 ChatGPT 生成 PlantUML 时,只有 80% 的结果是可正确编译的。通常来说,在这种时候会有多种不同的方式可以实现:
- 重新发送请求,以 GPT 再次生成。
- 告知 GPT 错了,让他重新生成。
从结果来看,两种模式的差异并不大。不过,第二种方式需要保持历史消息,所以会多消耗几个 token。
尽管从实践来看,第一种方式更为简单,但是方式二提供了一种更显式化的设计。
模式:动态代理调用
意图:支持动态代理调用其他服务的API或函数,并根据用户的输入进行匹配和响应。
适用场景:需要动态处理用户输入并调用相应功能的系统,以及需要将自然语言与函数或服务进行匹配和转换的系统。它为开发人员提供了一种灵活和可扩展的方式来处理不同的输入,并根据需求调用相应的API或函数。
示例 1:LangChain Agents 采用的便是类似的机制,它负责动态代理调用其他服务的API,比如精确计算、实际的业务数据 API 等。由内部提供的一系列问题模板来构建这个过程:
...
Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{tool_names}]
...
示例 2:OpenAI 提供的 Function calling,可以检测何时需要调用函数(取决于用户的输入)并使用符合函数签名(signature)的 JSON
进行响应。 这样一来,开发人员更可靠地从模型中获取结构化数据。诸如于官方示例里的:send_email(to: string, body: string)
,便是可以直接由用户的输入转换过来的。而在自然语言与函数之前,我们需要设计一个的方式来进行匹配。
示例 3:在 ArchGuard Co-mate 中,我们也构建了类似的方式,其缘由是架构治理是一个复杂的问题,我们确定用户的输入并不一定在系统之中。因此,我们希望提供两种方式:
- 反射 + 抽象的方式可调用的函数,来匹配用户的输入。
- 提供关键的模型信息, 让 LLM 进行分析。
所以,我们设计的是 DynamicContext 的方式,以及对应的 DyFunction 来实现这种方式的支持。
模式:本地函数动态代理
意图:旨在结合本地运行的 NLP 工具对用户输入的文本进行分析,并根据分析结果动态匹配对应的函数进行调用。
适用场景:根据用户输入的文本内容动态匹配并调用相应函数的系统,通过本地文本分析工具结合语义分析实现准确的函数匹配。它提供了灵活和可扩展的方式来处理不同的文本输入,并自动选择适当的函数进行处理。
示例:在 Co-mate 中,我们在本地引入了 SentenceTransformer 来处理用户的输入,优在本地分析、匹配用户的输入,并处理。当匹配到结果后直接调用本地的函数,当匹配不到结果时调用远端的处理函数来处理。