本文将介绍 Chocolate Factory 框架背后的一系列想法和思路。在我们探索和设计框架的过程中,受到了:LangChain4j、LangChain、LlamaIndex、Spring AI、Semantic Kernel、PromptFlow 的大量启发。
欢迎一起来探索:https://github.com/unit-mesh/chocolate-factory 。
在我们的参考架构里,框架/SDK 只是整体参考架构的一小部分。
在过去的几个月里,我们一直在结合已有的 JVM 体系基础设施构建 LLM 应用。诸如于:
这些尝试是围绕于我们过去的另外一个假设:当大模型成本降低,可靠性上升之后,AIGC 会与业务应用紧密结合。在这时,我们需要致力于:
简单来说,我们需要一些封装内部基础设施的框架、调试应用,以快速支撑 AI 应用的开发。
诸如于,我们在构建本地化代码搜索时,需要如下的工具:
dependencies {
// 核心库
implementation("cc.unitmesh:cocoa-core:0.3.4")
// 代码拆分
implementation("cc.unitmesh:code-splitter:0.3.4")
// Elastisearch 向量化存储,普通搜索
implementation("cc.unitmesh:store-elasticsearch:0.3.4")
// 本地化的 embedding,用于每次更新代码,重新 embedding => CPU
implementation("cc.unitmesh:sentence-transformers:0.3.4")
}
每个模块并不复杂,只是一些基本的 API 与基本的逻辑封装。但是,可以直接与现有的 AI 应用结合,再配套内部的 MaaS(模型即服务平台),就能快速在现有的业务中接入 LLM 能力。
如先前的文档所说,我们将 LLM 优先的软件架构分为三类:。
在当前,我们优先关注于结合向量化,也就是大部分 RAG 场景下的交互。即基于特定的业务场景,分析用户的意图,自动提供相应的上下文,交由大模型来进行自动化分析。对于这三种类型来说,或者不同场景时,各自的关注度是不一样的。
开发一个框架与过去的东西差别不多。但是,有意思的一点是,由于我们构建的是一个框架,所以当看到新的 RAG 论文,第一反应就是能否交由框架来支持。
诸如于 LangChain、LlamaIndex 在这方面已经做了非常好的抽象,我们在设计的时候,参考(复制)了大量的相关思想。在 LlamaIndex 的文档上,已经给出了很好的抽象,在这里我们使用我们新开发的 RAGScript (可以直接运行)来表达相关的内容:
rag {
indexing {
val document = document("filename.text")
val chunks = document.split()
store.indexing(chunks)
}
querying {
val results = store.findRelevant("Hello World").lowInMiddle()
llm.completion {
"// 结合 results 处理 prompt"
}
}
}
上面的代码段非常言简意赅的表达 RAG 的过程。简单来说,一个 RAG 分为 Indexing 和 Querying 两个阶段:
而现有的 LangChain、LlamaIndex 已经能提供我们很好的思路。唯一的挑战是,如何结合不同场景去探索合适的应用示例。
在不同的 LLM 应用开发框架或者 LLM 数据处理引擎里,都有大量的基础设施支持。但是,由于我们的场景往往是多种多样的,所以有时候显得有些不足。诸如于:
同时,为了更好的开发框架,除了结合过往开发 LLM 应用的经验,还得思考一些新的场景作为试验田。诸如于:
每种场景之下,对于框架的支持要求是不一样的。对于简单的场景,应该直接由 core 模块来提供所有的能力;对于复杂的场景,需要提供基本的 workflow 支持,以实现快速的应用开发。
除此,在复杂的场景之后,大部分的数据需要经过二次处理,也就是:
而在某些场景,我们需要的是长等待时间,即等待所有的用户返回内容。而对于这种不能由框架支持的功能,则稍微显得麻烦一些,我们得考虑通过示例向用户呈现这个过程。
在开发了基本的 Chocolate Factory 功能之后,为了更好的让其他同事参与进来。我们尝试编写一系列的文档和示例,以向其他人解释:如何开发一个基于 LLM 的 RAG 应用?
为此,我们基于已有的 API 能力,构建了 RAGScript,以快速向其他人解释完整的过程。完事的示例代码如下所示:
rag("code") {
// 使用 OpenAI 作为 LLM 引擎
llm = LlmConnector(LlmType.OpenAI)
// 使用 SentenceTransformers 作为 Embedding 引擎
embedding = EmbeddingEngine(EngineType.SentenceTransformers)
// 使用 Memory 作为 Retriever
store = Store(StoreType.Memory)
indexing {
// 从文件中读取文档
val document = document("filename.txt")
// 将文档切割成 chunk
val chunks = document.split()
// 建立索引
store.indexing(chunks)
}
querying {
// 进行相关性查询
val revelant = store.findRelevant("Hello World")
// 结合 Lost in the Middle 的长上下文重新排序
val results = revelant.lowInMiddle()
println(results)
}
}
当然了,在我们的 RAG 示例中,还提供了代码语义化解释搜索的示例。详细见:https://framework.unitmesh.cc/docs/rag 。
尽管我们开发的是模式二(Co-pilot 型应用)优先的 LLM 应用框架,但是在我们开发示例的过程中,我们依旧会发现对于 Prompt 的调试与编排是最大的挑战。
在 Chocolate Factory 的 DDD 思想的工作流中,我们推荐的实践是:
然而,对于一个大型的组织来说,内部会存在不同的 LLM。对于不同的 LLM 而言,相应的 prompt 编写模式也是有差异的。所以,我们正在思考采用同 PromptFlow 相似的方式,即采用独立的模板文件,结合工作流编排,以适应不同模式的 promtp 差异。如下是 PromptFlow 的模板示例:
system:
You are a helpful assistant.
{% for item in chat_history %}
user:
{{item.inputs.question}}
assistant:
{{item.outputs.answer}}
{% endfor %}
user:
{{question}}
再结合 CLI 或者 IDE 可视化的方式进行探索。
总的来说,这篇文章深入探讨了设计 JVM 语言的 LLM 应用开发框架的思考过程,强调了框架的多样性和复杂性,以及如何通过框架和工具来支持各种 LLM 应用场景。我们提供了有用的示例代码和抽象概念,有助于读者更好地理解和开发 LLM 应用。
围观我的Github Idea墙, 也许,你会遇到心仪的项目