Koog 的 Agent 需要通过 LLM 完成推理和生成。这篇文章主要讲解 Prompt、LLM parameters、LLM clients、Prompt executors 和 LLM providers 等概念。
一、概览
Prompt 是给 Large Language Models 的指令,用于指导模型生成响应。
Koog 为针对 LLM 运行 prompt 提供了两个抽象层级:LLM clients 和 prompt executors。 两者都接受 Prompt 对象,并且都可用于直接执行 prompt,而无需 AI agent。 clients 和 executors 的执行流程相同:
flowchart TB A(Prompt built with Kotlin DSL or Java builder) B{LLM client or prompt executor} CLLM provider D(Response to your application) A -->|"passed to"| B B -->|"sends request"| C C -->|"returns response"| B B -->|"returns result"| D
二、Prompt
Prompt 是给 Large Language Models 的指令,用于指导模型生成响应。
2.1 创建 Prompt
Koog 中的 Prompt 是 Prompt data class 的实例,包含:
id:Prompt 的唯一标识。messages:与 LLM 对话的消息列表。params:可选的 LLM 配置参数,例如 temperature、tool choice 等。
虽然我们可以直接实例化 Prompt class,但推荐使用 Kotlin DSL 或 Java builder API 创建 Prompt,它们提供了一种结构化的方式来定义对话提示词。
import ai.koog.prompt.dsl.prompt
val myPrompt = prompt("hello-koog") {
system("You are a helpful assistant.")
user("What is Koog?")
}
另外:AI agents 可以接收简单文本 prompt 作为输入。 它们会自动将文本 prompt 转换为 Prompt 对象,并将其发送给 LLM 执行。 这对于 Basic Agent 很有用, 它只需要运行单个请求,不需要复杂的对话逻辑。
2.2 消息类型
Kotlin DSL 和 Java builder API 支持以下消息类型,每种类型都对应对话中的特定角色:
-
System message: 系统消息定义 LLM 的行为,并为整个对话设置上下文。 它可以指定模型的角色和语气,为响应提供准则和约束,并提供响应示例。
-
User message: 用户消息表示来自用户的输入。大多数用户消息包含纯文本,但也可以包含多模态内容,例如图像、音频、视频和文档。 后面的文章会讲解。
-
Assistant message: 助手消息表示 LLM 响应的内容,可用于未来类似交互中的 few-shot 学习、继续对话,或展示预期的输出结构。可包含文本回复、推理过程等等。在多轮对话中, Assistant 消息会被追加到消息历史里, 作为上下文回传给 LLM, 让模型"记住"自己之前说过什么。
-
Tool message: 工具消息表示工具调用及其结果,可用于预填充工具调用历史。
import ai.koog.prompt.dsl.prompt
val myPrompt = prompt("unique_prompt_id") {
// Add a system message to set the context
system("You are a helpful assistant with access to tools.")
// Add a user message
user("What is 5 + 3 ?")
// Add an assistant message
assistant("The result is 8.")
}
2.3 文本消息构建器
注意:文本消息构建器仅在 Kotlin 中可用。
构建 system()、user() 或 assistant() 消息时,可以使用辅助text-building-functions 进行富文本格式化。
val prompt = prompt("text_example") {
user {
+"Review the following code snippet:"
+"fun greet(name: String) = println(\"Hello, \$name!\")"
// Paragraph break
br()
text("Please include in your explanation:")
// Indent content
padding(" ") {
+"1. What the function does."
+"2. How string interpolation works."
}
}
}
也可以使用 Markdown 和 XML 构建器,以相应格式添加内容:
val prompt = prompt("markdown_xml_example") {
// A user message in Markdown format
user {
markdown {
h2("Evaluate the article using the following criteria:")
bulleted {
item { +"Clarity and readability" }
item { +"Accuracy of information" }
item { +"Usefulness to the reader" }
}
}
}
// An assistant message in XML format
assistant {
xml {
xmlDeclaration()
tag("review") {
tag("clarity") { text("positive") }
tag("accuracy") { text("neutral") }
tag("usefulness") { text("positive") }
}
}
}
}
2.4 提示词参数
可以通过配置控制 LLM 行为的参数来自定义提示词。
val prompt = prompt(
id = "custom_params",
params = LLMParams(
temperature = 0.7,
numberOfChoices = 1,
toolChoice = LLMParams.ToolChoice.Auto
)
) {
system("You are a creative writing assistant.")
user("Write a song about winter.")
}
关于 LLMParams 的详细内容,将在后面的文章讲解。
2.5 扩展现有提示词
可以通过在 Kotlin 中调用 prompt() 函数,或在 Java 中调用 Prompt.builder() 并传入现有提示作为参数来扩展现有提示:
val basePrompt = prompt("base") {
system("You are a helpful assistant.")
user("Hello!")
assistant("Hi! How can I help you?")
}
val extendedPrompt = prompt(basePrompt) {
user("What's the weather like?")
}
最终 extendedPrompt 的 Message 列表相当于:
[
SystemMessage("You are a helpful assistant."),
UserMessage("Hello!"),
AssistantMessage("Hi! How can I help you?"),
UserMessage("What's the weather like?")
]
请求 LLM 时会转换成类似下面这样的 Chat API 请求:
{
"messages": [
{
"role": "system",
"content": "You are a helpful assistant."
},
{
"role": "user",
"content": "Hello!"
},
{
"role": "assistant",
"content": "Hi! How can I help you?"
},
{
"role": "user",
"content": "What's the weather like?"
}
]
}
值得注意的是:这个例子最后不是"一个最终字符串 Prompt",而是一个有 4 条 Message 的对话上下文。理解这一点很重要。
三、LLM clients
LLM clients 专为与 LLM providers 直接交互而设计。 每个 client 都实现 LLMClient 接口,该接口提供执行 prompts 和流式响应的方法。
当你使用单个 LLM provider 且不需要高级生命周期管理时,可以使用 LLM client。 如果需要管理多个 LLM providers,请使用 prompt executor。
LLM clients 针对不同的 LLM provider 的能力有一些细微差别,具体可以查看官方文档。
运行 prompt
使用 LLM client 运行 prompt,有一下两个步骤:
- 创建一个 LLM client,用于处理你的应用与 LLM providers 之间的连接。
- 使用 prompt 和 LLM 作为参数调用 execute() 方法。
下面是一个使用 OpenAILLMClient 运行 prompts 的示例:
val deepSeekClient = DeepSeekLLMClient(apiKey = deepseekApiKey)
val myPrompt = prompt("test-llmclients") {
system("You are a helpful assistant.")
user("Tell me about Koog")
}
// 运行提示词
val response = deepSeekClient.execute(myPrompt, DeepSeekModels.DeepSeekV4Flash)
println(response)
3.1 流式响应
流式响应使用与所有的 LLM clients。当需要在响应生成时处理响应,可以在 Kotlin 中使用 executeStreaming() 方法,或在 Java 中使用 executeStreamingWithPublisher() 来流式传输模型输出。
流式 API 提供不同的 frame 类型:
Delta frames(TextDelta, ReasoningDelta, ToolCallDelta) ------ 以分块形式到达的增量内容Complete frames(TextComplete, ReasoningComplete, ToolCallComplete) ------ 收到所有 delta 后的完整内容End frame(End) ------ 通过 finish reason 标记流完成
对于支持 reasoning 的模型(例如 Claude Sonnet 4.5 或 GPT-o1),流式传输期间会发出 reasoning frames。
关于流式 API ,后面的文章单独讲解。
3.2 Multiple choices
在 Koog 里,executeMultipleChoices 可以简单理解为:一次 Prompt 请求,让 LLM 让模型独立生成多个候选答案(multiple choices),然后把这些候选结果一起返回。
GoogleLLMClient、BedrockLLMClient 和 OllamaClient 不适用。
可以使用 executeMultipleChoices() 方法在单次调用中请求模型生成多个备选响应。 它要求在正在执行的 prompt 中指定 numberOfChoices LLM 参数。
val client = OpenAILLMClient(apiKey)
val choices = client.executeMultipleChoices(
prompt = prompt("n_best", params = LLMParams(numberOfChoices = 3)) {
system("You are a creative assistant.")
user("Give me three different opening lines for a story.")
},
model = OpenAIModels.Chat.GPT4o
)
choices.forEachIndexed { i, choice ->
val text = choice.parts.filterIsInstance<MessagePart.Text>().joinToString(" ") { it.text }
println("Line #${i + 1}: $text")
}
注意:不过这里有一个很容易混淆的地方:它不一定意味着"让模型思考后给你 3 个不同答案"。
概念上相当于告诉模型:针对同一个 Prompt,给我生成 3 个候选结果。可能得到:
Choice 1:
Hello! How can I help you today?
Choice 2:
Hi there! What can I assist you with?
Choice 3:
Hello! I'd be happy to help. What do you need?
因此返回值通常是多个生成结果的集合,而不是单个 Message。
3.3 列出可用模型
AnthropicLLMClient、BedrockLLMClient 和 OllamaClient 不适用。
要获取 LLM client 支持的可用模型 ID 列表,使用 models() 方法即可。
private suspend fun testlistModels(deepseekApiKey: String) {
val deepSeekClient = DeepSeekLLMClient(apiKey = deepseekApiKey)
val models = deepSeekClient.models()
models.forEach { model -> println(model.id) }
}
输出结果:
deepseek-flash
deepseek-v4-pro
3.4 Embeddings
适用于 OpenAILLMClient、GoogleLLMClient、BedrockLLMClient、MistralAILLMClient 和 OllamaClient。
在 Koog 中,embed() 可以理解成:
把一段文本转换成一个"向量(Embedding)",用于让程序能够计算文本之间的语义相似度。
使用 embed() 方法将文本转换为 embedding 向量。 选择一个 embedding 模型,并将文本传给此方法:
val embedding = client.embed(
text = ""Kotlin is a programming language."",
model = OpenAIModels.Embeddings.TextEmbedding3Large
)
得到的结果是一串浮点数,比如:
[
0.0123,
-0.0842,
0.2311,
...
0.0567
]
例如一个模型可能产生 768 维向量。这些数字本身通常没有直接的人类可读意义,但整个向量可以表示这段文本的语义特征。最典型的用途就是 语义搜索 和 RAG / 知识库。
值得理解的两个点:
-
embed()并不是让 LLM"理解"文本。Embedding Model 和 Chat Model 通常是两类模型。所以可以把embed()理解成 Koog 提供的"文本 → 向量"的基础 AI 能力,而不是"让 Agent 思考"的能力。 -
embed()不是 Agent 专属能力。Agent 可以使用它,但 Embedding 本身并不需要 Agent。
3.5 Moderation
适用于 OpenAILLMClient、BedrockLLMClient、MistralAILLMClient、OllamaClient。
Koog 中的 Moderation 可以理解成:
在用户输入或模型输出进入下一步之前,对内容进行安全/合规检查。
将 moderate() 方法与 moderation 模型一起使用,检查 prompt 是否包含不适当内容:
val result = client.moderate(
prompt = prompt("moderation") {
user("This is a test message that may contain offensive content.")
},
model = OpenAIModels.Moderation.Omni
)
Moderation 通常检查什么?
具体取决于你使用的 Moderation Provider / 模型,但通常会涉及一些内容安全类别,例如:
Violence
Hate
Harassment
Sexual content
Self-harm
Illegal activities
可以理解为:
flowchart TD A用户输入 --> BModeration B -->|允许| CAgent B -->|拒绝| DBlock C --> ELLM
实际应用中,可以对输入和输出分别做 Moderation。
Moderation 和普通 Prompt 判断的区别
你也可以直接让 LLM 判断:
请判断下面内容是否安全:
...
但 Moderation API 通常是专门针对内容安全设计的接口。得到的可能是结构化的结果。而不是:
"这段内容看起来没问题。"
这对程序来说更容易处理。
四、Prompt executors
Prompt executors 管理一个或多个 LLM clients 生命周期的高级抽象。 当你需要一个统一 API 来跨多个 providers 运行 prompt,屏蔽 provider 特定细节,并在它们之间动态切换和使用回退。它们推荐用于生产环境,因为在使用多个 providers 时可提供灵活性。
4.1 Executor 类型
Koog 提供三种实现 PromptExecutor 接口的主要 prompt executors:
| 类型 | 类 | 描述 |
|---|---|---|
| Single-provider | SingleLLMPromptExecutor | 封装单个 provider 的单个 LLM client。如果你的 agent 只需要在单个 LLM provider 内的模型之间切换,请使用此 executor。 |
| Multi-provider | MultiLLMPromptExecutor | 封装多个 LLM clients,并根据 LLM provider 路由调用。当请求的 client 不可用时,它可以选择使用已配置的 fallback provider 和 LLM。如果你的 agent 需要在不同 providers 的 LLM 之间切换,请使用此 executor。 |
| Routing | RoutingLLMPromptExecutor | 使用路由策略将对给定 LLM model 的请求分发到多个 client instances。使用此 executor 可避免速率限制、提高吞吐量,并通过负载均衡实现故障转移策略。 |
4.2 创建 single-provider executor
要为特定 LLM provider 创建 prompt executor,有如下两个步骤:
-
使用相应的 API key 为特定 provider 配置 LLM client。
-
使用 MultiLLMPromptExecutor 创建 prompt executor。
val deepSeekClient = DeepSeekLLMClient(apiKey = deepseekApiKey)
val promptExecutor = MultiLLMPromptExecutor(deepSeekClient)
4.3 创建 multi-provider executor
要创建可与多个 LLM providers 配合使用的 prompt executor,需如下两个步骤:
-
使用相应 API keys 为所需 LLM providers 配置 clients。
-
将配置好的 clients 传入
MultiLLMPromptExecutor类构造函数,以创建带有多个 LLM providers 的 prompt executor。val ollamaClient = OllamaClient()
val deepSeekClient = DeepSeekLLMClient(apiKey = deepseekApiKey)
val multiExecutor = MultiLLMPromptExecutor(
LLMProvider.Ollama to ollamaClient,
LLMProvider.DeepSeek to deepSeekClient
)
4.4 创建 routing executor
当前 Routing 能力是实验性的,未来版本中可能会发生变化。 要使用该 API,需要使用 @OptIn(ExperimentalRoutingApi::class) 选择加入。
创建一个使用路由策略在多个 LLM client instances 之间分发请求的 prompt executor,续一下三个步骤:
- 配置多个 client instances(它们可以属于相同或不同的 LLM providers),并设置相应 API keys。
- 使用路由策略创建 router,例如
RoundRobinRouter。 - 将 router 传入 RoutingLLMPromptExecutor 类构造函数。
这对于避免速率限制、提高吞吐量以及实现故障转移策略很有用。
val ollamaClient = OllamaClient()
val deepSeekClient1 = DeepSeekLLMClient(apiKey = deepseekApiKey)
val deepSeekClient2 = DeepSeekLLMClient(apiKey = deepseekApiKey)
val router = RoundRobinRouter(ollamaClient, deepSeekClient1, deepSeekClient2)
val routingExecutor = RoutingLLMPromptExecutor(router)
当你使用此 executor 执行 prompts 时,对 deepseek models 的请求会使用 round-robin 策略在 deepSeekClient1 和 deepSeekClient2 之间交替。 对 ollamaClient models 的请求始终会发送到单个 ollama client,因为 round-robin 会为每个 provider 维护独立计数器。
可以通过创建实现 LLMClientRouter 接口的类来实现自定义路由策略。
4.5 预定义 prompt executors
为了更快设置,Koog 为常见 providers 在 Kotlin 和 Java 中提供了开箱即用的 executor 实现。
下表包含预定义 single-provider executors,它们返回配置了特定 LLM client 的 SingleLLMPromptExecutor。
| LLM provider | Prompt executor | 描述 |
|---|---|---|
| OpenAI | simpleOpenAIExecutor | 封装运行 OpenAI models prompts 的 OpenAILLMClient。 |
| OpenAI | simpleAzureOpenAIExecutor | 封装配置为使用 Azure OpenAI Service 的 OpenAILLMClient。 |
| Anthropic | simpleAnthropicExecutor | 封装运行 Anthropic models prompts 的 AnthropicLLMClient。 |
| simpleGoogleAIExecutor | 封装运行 Google models prompts 的 GoogleLLMClient。 |
|
| OpenRouter | simpleOpenRouterExecutor | 封装运行 OpenRouter prompts 的 OpenRouterLLMClient。 |
| Amazon Bedrock | simpleBedrockExecutor | 封装运行 AWS Bedrock prompts 的 BedrockLLMClient。 |
| Amazon Bedrock | simpleBedrockExecutorWithBearerToken | 封装 BedrockLLMClient,并使用提供的 Bedrock API key 发送请求。 |
| Mistral | simpleMistralAIExecutor | 封装运行 Mistral models prompts 的 MistralAILLMClient。 |
| Ollama | simpleOllamaAIExecutor | 封装运行 Ollama prompts 的 OllamaClient。 |
使用方式:
val promptExecutor = simpleOpenAIExecutor("OPENAI_API_KEY")
4.6 执行 Prompt
使用 prompt executor 运行 prompt,需如下两步操作:
-
创建 prompt executor。
-
使用 execute() 方法通过特定 LLM 运行 prompt。
val deepSeekClient = DeepSeekLLMClient(apiKey = deepseekApiKey)
val promptExecutor = MultiLLMPromptExecutor(deepSeekClient)
promptExecutor.execute(
prompt = prompt("demo") { user("Summarize this.") },
model = DeepSeekModels.DeepSeekV4Flash
)
由于 Prompt executor 是对于 LLM Clients 的包装,所以每个 executor 都支持响应 client 的能力。例如:流式生成、多选生成、内容审核。上一节已经详细解释了,这里不再赘述。
4.7 在 providers 之间切换
当使用 MultiLLMPromptExecutor 与多个 LLM providers 协作时,可以在它们之间切换。 流程如下:
- 为你想使用的每个 provider 创建一个 LLM client instance。
- 创建一个将 LLM providers 映射到 LLM clients 的 MultiLLMPromptExecutor。
- 使用 execute() 方法运行 prompt,并将来自相应 client 的 model 作为参数传入。 prompt executor 会根据 model provider 使用对应 client 来运行 prompt。
下面是在 providers 之间切换的示例:
val ollamaClient = OllamaClient()
val deepSeekClient = DeepSeekLLMClient(apiKey = deepseekApiKey)
val multiExecutor = MultiLLMPromptExecutor(
LLMProvider.Ollama to ollamaClient,
LLMProvider.DeepSeek to deepSeekClient
)
val p = prompt("demo") { user("Summarize this.") }
val allamaResult = executor.execute(p, Ollama.Chat.Qwen2.5-0.5B)
val deepseekResult = executor.execute(p, DeepSeekModels.DeepSeekV4Flash)
4.8 配置回滚策略
Multi-provider 和 routing prompt executors 可以配置为在请求的 LLM client 不可用时使用 fallback LLM provider 和 model。
配置 fallback 机制,只需在创建 MultiLLMPromptExecutor 或 RoutingLLMPromptExecutor 时传入 fallback 设置:
val ollamaClient = OllamaClient()
val deepSeekClient = DeepSeekLLMClient(apiKey = deepseekApiKey)
val multiExecutor = MultiLLMPromptExecutor(
LLMProvider.Ollama to ollamaClient,
LLMProvider.DeepSeek to deepSeekClient
fallback = MultiLLMPromptExecutor.FallbackPromptExecutorSettings(
fallbackProvider = LLMProvider.Ollama,
fallbackModel = OllamaModels.Meta.LLAMA_3_2
)
)
如果你传入的 model 来自未包含在 MultiLLMPromptExecutor 中的 LLM provider, prompt executor 会使用 fallback model。
Fallbacks 仅适用于 execute() 和 executeMultipleChoices() 方法。
五、HTTP clients
Koog 中的每个 LLM client 都需要一个 KoogHttpClient ------ 这是框架用于与 providers 通信的抽象 HTTP 契约。在构造时传入它。简单说就是:Koog 用来和外部 HTTP/HTTPS 服务通信的网络客户端层。它本身不是 Agent,也不是 LLM,而是 Koog 底层的网络通信基础设施。
5.1 为什么 Koog 需要 HTTP Client
比如我的 Agent 使用 DeepSeek:
Koog Agent
↓
PromptExecutor
↓
DeepSeek LLM Client
↓
HTTP Client
↓
HTTPS
↓
DeepSeek API
最终本质上还是发送 HTTP 请求:
POST https://api.deepseek.com/chat/completions
Authorization: Bearer xxx
Content-Type: application/json
HTTP Client 就负责真正的:
- 建立连接
- 发送 HTTP Request
- 接收 HTTP Response
- HTTPS/TLS
- 超时
- 连接池
- 网络错误处理等
5.2 Koog 为什么单独抽象 HTTP Client
因为 Koog 是 Kotlin Multiplatform, 它可以运行在不同的平台:Android、iOS、JS、WasmJS 等等。不同平台的 HTTP 实现并不一样。所以 Koog 不希望 Agent 代码直接依赖某一个平台的 HTTP 实现。
5.3 HTTP Client 和 LLM Client 的区别
这个区别非常重要:
- HTTP Client 只关心
HTTP 请求和HTTP 响应。它并不知道什么叫 Prompt、Token、Agent 这些概念。 - LLM Client 关注 Model、 Prompt、 Message、 Token、 Streaming、 Tool Call 等等这些概念。并负责吧这些内容转换成具体的 Provider 的 HTTP API。
5.4 Koog 内置的 HTTP client
可以自己构建 KoogHttpClient,但这需要不少工作:每个 provider 都有自己的 base URL、auth header 形式、content-type 和 SSE 约定。针对每个 provider 正确处理所有这些细节,正是 KoogHttpClient.Factory 要替你省去的工作。你传入一个 Factory,provider client 会用适合其 API 的参数调用 Factory.create(...)。
Koog 提供了开箱即用的四种后端 factory ------ Ktor、JDK HttpClient、OkHttp 和 Spring 的 WebClient ------ 你也可以实现自己的 factory。
| 模块 | 说明 |
|---|---|
| http-client-ktor | 唯一可从非 JVM target 使用的后端。 |
| http-client-java | 包装 JDK 11+ 的 java.net.http.HttpClient。 |
| http-client-okhttp | 基于 OkHttp,Android 友好。 |
| http-client-spring-webclient | 基于 Spring WebClient。 |
一个 factory 可用于任何 provider:选择一次后端,并在各个 clients 中复用它。
val factory = KtorKoogHttpClient.Factory()
val openai = OpenAILLMClient(
apiKey = System.getenv("OPENAI_API_KEY"),
settings = OpenAIClientSettings(),
httpClientFactory = factory,
)
val anthropic = AnthropicLLMClient(
apiKey = System.getenv("ANTHROPIC_API_KEY"),
settings = AnthropicClientSettings(),
httpClientFactory = factory,
)
5.5 HTTP client factory 自动发现
细心的你可能已经发现了,前面我们在构造 LLM Client 时,并没有传入 httpClientFactory 参数。实际上这是因为 Koog 在 JVM 和 Android 上,构造每个 LLM client,无需显式传入 factory。Koog 在在幕后通过 HttpClientFactoryResolver 使用 java.util.ServiceLoader 从运行时 classpath 解析 KoogHttpClient.Factory:
- 每个后端模块都提供一个 ServiceLoader 注册。
- 只有当运行时 classpath 中恰好可见一个 factory 时,解析才会成功。
- prompt-executor-llms-all 将 http-client-ktor 声明为 runtimeOnly 依赖,因此默认会得到 Ktor,而不会在编译期暴露该模块。
- simple Executor(apiKey) 和 PromptExecutorBuilder.(apiKey) 使用相同的解析路径。
目前 KMP 不支持自动发现,因此 JVM 之外也无法使用这些便捷方法。从 commonMain 中使用时,请显式传入 Factory。
自动发现注意事项:
- 运行时 classpath 上没有后端 → 首次解析时抛出 IllegalStateException。请将后端模块添加到运行时 classpath,或显式传入 Factory。
- 两个或更多后端 → 同样抛出异常;消息会列出找到的 providers。使用 Gradle 排除到只剩一个(在有问题的依赖上使用 exclude(module = "http-client-ktor")),或在调用处显式传入 Factory。
5.6 自定义 HttpClient.Factory
任何实现 KoogHttpClient.Factory 的类都可以使用。若要使其在 JVM 上可自动发现,请将其注册为 ServiceLoader provider:
src/main/resources/META-INF/services/ai.koog.http.client.KoogHttpClient$Factory
该文件包含一行内容:factory 类的完全限定名。字面量 (嵌套 Factory 类的分隔符)是正确的 ------ 文件名是 KoogHttpClientFactory,不是 KoogHttpClient.Factory。
如果不想使用自动发现,跳过注册即可,并在所有位置显式传入你的 factory。
六、异常机制
Koog 有内置的重试和超时机制来处理 LLM clients 和 prompt executors 的失败。
6.1 重试功能
使用 LLM providers 时,可能会出现速率限制或临时服务不可用等瞬时错误。 RetryingLLMClient 装饰器会为 Kotlin 和 Java 中的任何 LLM client 添加自动重试逻辑。
6.1.1 基本用法
可以使用用 RetryingLLMClient 包装任何现有的 LLM Client。
val deepSeekClient = DeepSeekLLMClient(apiKey = deepseekApiKey)
val resilientClient = RetryingLLMClient(deepSeekClient)
resilientClient.execute(promot, DeepSeekModels.DeepSeekV4Flash)
6.1.2 配置重试行为
默认情况下,RetryingLLMClient 会为 LLM client 配置最多 3 次重试、1 秒初始延迟, 以及 30 秒最大延迟。 你可以通过传给 RetryingLLMClient 的 RetryConfig 指定不同的重试配置。
val conservativeClient = RetryingLLMClient(
delegate = client,
config = RetryConfig.CONSERVATIVE
)
Koog 提供了几种预定义重试配置,可在 Kotlin 中通过 RetryConfig 使用,在 Java 中通过 RetryConfig.Companion 使用:
| 配置 | 最大尝试次数 | 初始延迟 | 最大延迟 | 使用场景 |
|---|---|---|---|---|
| DISABLED | 1 | - | - | 开发、测试和调试 |
| CONSERVATIVE | 3 | 2s | 30s | 后台或计划任务 |
| AGGRESSIVE | 5 | 500ms | 20s | 关键操作 |
| PRODUCTION | 3 | 1s | 20s | 一般生产用途 |
也可以创建自定义配置:
val customClient = RetryingLLMClient(
delegate = client,
config = RetryConfig(
maxAttempts = 5,
initialDelay = 1.seconds,
maxDelay = 30.seconds,
backoffMultiplier = 2.0,
jitterFactor = 0.2
)
)
6.1.3 重试错误模式
默认情况下,RetryingLLMClient 会识别常见的瞬时错误。 此行为由 RetryConfig.retryablePatterns 模式控制。 每个模式都由 RetryablePattern 表示,它会检查失败请求的错误消息,并判断是否应重试。
Koog 提供了可跨所有受支持 LLM providers 使用的预定义重试配置和模式。 可以保留默认值,也可以针对特定需求自定义它们。
6.1.3.1 模式类型
可以使用以下模式类型,并组合任意数量的模式:
- RetryablePattern.Status: 匹配错误消息中的特定 HTTP 状态码(例如 429、500、502 等)。
- RetryablePattern.Keyword: 匹配错误消息中的关键字(例如 rate limit 或 request timeout)。
- RetryablePattern.Regex: 匹配错误消息中的正则表达式。
- RetryablePattern.Custom: 使用 lambda 函数匹配自定义逻辑。
如果任意模式返回 true,该错误会被视为可重试,LLM client 会重试请求。
6.1.3.2 默认模式
除非你自定义重试配置,否则默认使用以下模式:
HTTP 状态码:
| 状态码 | 含义 |
|---|---|
| 429 | Rate limit |
| 500 | Internal server error |
| 502 | Bad gateway |
| 503 | Service unavailable |
| 504 | Gateway timeout |
| 529 | Anthropic overloaded |
错误关键字:
| 错误类型 | 含义 |
|---|---|
| Rate limit | 请求频率超过限制 |
| Too many requests | 请求数量过多 |
| Request timeout | 请求超时 |
| Connection timeout | 连接超时 |
| Read timeout | 读取响应超时 |
| Write timeout | 写入请求超时 |
| Connection reset by peer | 对端重置连接 |
| Connection refused | 连接被拒绝 |
| Temporarily unavailable | 临时不可用 |
| Service unavailable | 服务不可用 |
这些默认模式在 Koog 中定义为 RetryConfig.DEFAULT_PATTERNS。
6.1.3.3 自定义模式
Koog 允许针对特定需求定义自定义模式:
val config = RetryConfig(
retryablePatterns = listOf(
RetryablePattern.Status(429), // Specific status code
RetryablePattern.Keyword("quota"), // Keyword in error message
RetryablePattern.Regex(Regex("ERR_\\d+")), // Custom regex pattern
RetryablePattern.Custom { error -> // Custom logic
error.contains("temporary") && error.length > 20
}
)
)
也可以将自定义模式追加到默认的:
val config = RetryConfig(
retryablePatterns = RetryConfig.DEFAULT_PATTERNS + listOf(
RetryablePattern.Keyword("custom_error")
)
)
6.1.4 流式传输与重试
流式操作可以选择重试。此功能默认禁用。
val config = RetryConfig(
maxAttempts = 3
)
val client = RetryingLLMClient(baseClient, config)
val stream = client.executeStreaming(prompt, OpenAIModels.Chat.GPT4o)
注意
流式重试仅适用于收到第一个 token 之前发生的连接失败。 一旦流式传输开始,重试逻辑就会被禁用。 如果在流式传输期间发生错误,操作会终止。
6.1.5 prompt executors 中的重试
使用 prompt executors 时,可以先用重试机制包装底层 LLM client,然后再创建 executor;Kotlin 和 Java 都支持。
val deepSeekClient = DeepSeekLLMClient(apiKey = deepseekApiKey)
val resilientClient = RetryingLLMClient(
deepSeekClient,
RetryConfig.PRODUCTION
)
val executor = MultiLLMPromptExecutor(resilientClient)
// Multi-provider executor
val multiExecutor = MultiLLMPromptExecutor(
LLMProvider.OpenAI to RetryingLLMClient(
OpenAILLMClient(System.getenv("OPENAI_API_KEY")),
RetryConfig.CONSERVATIVE
),
LLMProvider.Anthropic to RetryingLLMClient(
AnthropicLLMClient(System.getenv("ANTHROPIC_API_KEY")),
RetryConfig.AGGRESSIVE
),
// The Bedrock client already has a built-in AWS SDK retry
LLMProvider.Bedrock to BedrockLLMClient(
identityProvider = StaticCredentialsProvider {
accessKeyId = System.getenv("AWS_ACCESS_KEY_ID")
secretAccessKey = System.getenv("AWS_SECRET_ACCESS_KEY")
sessionToken = System.getenv("AWS_SESSION_TOKEN")
},
),
)
6.2 超时配置
所有 LLM clients 在 Kotlin 和 Java 中都支持超时配置,以防止请求挂起。 可以在创建 client 时使用 ConnectionTimeoutConfig 类指定网络连接的超时值。
ConnectionTimeoutConfig 具有以下属性:
| 属性 | 默认值 | 描述 |
|---|---|---|
connectTimeoutMillis |
60 秒 (60,000) | 建立到服务器连接的最长时间。 |
requestTimeoutMillis |
15 分钟 (900,000) | 完成整个请求的最长时间。 |
socketTimeoutMillis |
15 分钟 (900,000) | 在已建立连接上等待数据的最长时间。 |
可以针对特定需求自定义这些值。例如:
val deepSeekClient = DeepSeekLLMClient(
apiKey = deepseekApiKey,
settings = OpenAIClientSettings(
timeoutConfig = ConnectionTimeoutConfig(
connectTimeoutMillis = 5000, // 5 seconds to establish connection
requestTimeoutMillis = 60000, // 60 seconds for the entire request
socketTimeoutMillis = 120000 // 120 seconds for data on the socket
)
)
)
提示:
对于长时间运行或流式调用,可以为 requestTimeoutMillis 和 socketTimeoutMillis 设置更高的值。
6.3 错误处理
在生产环境中使用 LLM 时,需要实现错误处理,包括:
- Try-catch blocks 来处理意外错误。
- Logging errors with context 以便调试。
- Fallbacks 用于关键操作。
- Monitoring retry patterns 以识别重复出现的问题。
示例:
val logger = LoggerFactory.getLogger("Example")
val resilientClient = RetryingLLMClient(
OpenAILLMClient(System.getenv("OPENAI_API_KEY")),
RetryConfig.PRODUCTION
)
val prompt = prompt("test") { user("Hello") }
val model = OpenAIModels.Chat.GPT4o
fun processResponse(response: Any) { /* implmenentation */ }
fun scheduleRetryLater() { /* implmenentation */ }
fun notifyAdministrator() { /* implmenentation */ }
fun useDefaultResponse() { /* implmenentation */ }
try {
val response = resilientClient.execute(prompt, model)
processResponse(response)
} catch (e: Exception) {
logger.error("LLM operation failed", e)
when {
e.message?.contains("rate limit") == true -> {
// Handle rate limiting specifically
scheduleRetryLater()
}
e.message?.contains("invalid api key") == true -> {
// Handle authentication errors
notifyAdministrator()
}
else -> {
// Fall back to an alternative solution
useDefaultResponse()
}
}
}
七、LLM 缓存响应
对于使用 prompt executor 运行的重复请求, 可以在 Kotlin 和 Java 中缓存 LLM 响应,以优化性能并降低成本。 在 Koog 中,所有 prompt executors 都可以通过 CachedPromptExecutor 使用缓存, 它是围绕 PromptExecutor 的包装器,会添加缓存功能。 它允许你存储之前执行过的 prompts 的响应,并在再次运行相同 prompts 时检索这些响应。
要在 Kotlin 或 Java 中创建缓存的 prompt executor,请执行以下操作:
- 创建一个你想为其缓存响应的 prompt executor。
- 通过提供所需缓存和你创建的 prompt executor,创建 CachedPromptExecutor 实例。
- 使用所需 prompt 和 model 运行创建的 CachedPromptExecutor。
示例:
// Create a prompt executor
val client = OpenAILLMClient(System.getenv("OPENAI_API_KEY"))
val promptExecutor = MultiLLMPromptExecutor(client)
// Create a cached prompt executor
val cachedExecutor = CachedPromptExecutor(
cache = FilePromptCache(Path("path/to/your/cache/directory")),
nested = promptExecutor
)
// Run cached prompt executor for the first time
// This will perform an actual LLM request
val firstTime = measureTimeMillis {
val firstResponse = cachedExecutor.execute(prompt, OpenAIModels.Chat.GPT4o)
val text = firstResponse.parts.filterIsInstance<MessagePart.Text>().joinToString("\n") { it.text }
println("First response: $text")
}
println("First execution took: ${firstTime}ms")
// Run cached prompt executor for the second time
// This will return the result immediately from the cache
val secondTime = measureTimeMillis {
val secondResponse = cachedExecutor.execute(prompt, OpenAIModels.Chat.GPT4o)
val text = secondResponse.parts.filterIsInstance<MessagePart.Text>().joinToString("\n") { it.text }
println("Second response: $text")
}
println("Second execution took: ${secondTime}ms")
该示例会产生以下输出:
First response: Hello! It seems like we're starting a new conversation. What can I help you with today?
First execution took: 48ms
Second response: Hello! It seems like we're starting a new conversation. What can I help you with today?
Second execution took: 1ms
第二次响应从缓存中检索,仅耗时 1ms。
注意
- 如果在 Kotlin 中调用
executeStreaming()或在 Java 中调用executeStreamingWithPublisher()并使用缓存的 prompt executor,它会以单个 chunk 生成响应。 - 如果在 Kotlin 或 Java 中使用缓存的 prompt executor 调用 moderate(),它会将请求转发给嵌套的 prompt executor,并且不使用缓存。
- Kotlin 和 Java 都不支持缓存多选项响应(executeMultipleChoices())。
八、小结
Koog 的模型调用层包括 Prompt、LLM parameters、LLM clients、Prompt executors 和 providers。
Prompt 表达消息和参数;LLM clients 连接具体 provider;Prompt executors 提供更高层统一抽象;LLM providers 决定底层模型来源。不同 provider 的能力需要根据官方文档确认。
参考资料
- Prompts: https://docs.koog.ai/prompts/
- Prompt creation: https://docs.koog.ai/prompts/prompt-creation/
- LLM parameters: https://docs.koog.ai/llm-parameters/
- LLM clients: https://docs.koog.ai/prompts/llm-clients/
- Prompt executors: https://docs.koog.ai/prompts/prompt-executors/
- LLM providers: https://docs.koog.ai/llm-providers/
- Model capabilities: https://docs.koog.ai/model-capabilities/
- Streaming API: https://docs.koog.ai/streaming-api/