Koog(8)——Agent工程化:自定义 Feature、事件追踪与测试 - SharpCJ

Koog(8)——Agent工程化:自定义 Feature、事件追踪与测试 - SharpCJ

💡 原文中文,约36300字,阅读约需87分钟。
📝

内容提要

本文以 Koog 1.3.0 的 Kotlin API 为例,讲解 Agent 的事件与拦截器底座。自定义 feature 需实现对应 agent 类型的接口,并通过 install 注册拦截器,可覆盖 agent、策略、LLM 调用、流式、工具调用等生命周期;节点级拦截器仅限 graph 管道。事件对象可由 EventHandler 回调或 FeatureMessageProcessor 消费,Tracing 将事件转为消息交给 processor,OpenTelemetry 再将其翻译为 OTel span。内容审核是模型调用层的独立能力,不依赖 feature。序列化默认使用 KotlinxSerializer,可切换为 JacksonSerializer。测试可用官方 agents-test 的 mock 执行器离线验证。

🔎

延伸解读

拦截器不是免费的:注册范围决定开销

文章强调每个拦截器都有调用开销,长运行 agent 上全量注册并不划算,官方也建议只注册需要的拦截器。更值得注意的是回调是 suspend lambda,会被执行流程等待,慢网络操作会直接拉长 agent 耗时,且 suspend 不会把阻塞 IO 变成非阻塞。因此监控类逻辑应控制耗时并明确异常策略,避免观测代码反过来影响主流程。

两层过滤别混用:feature 过滤与 processor 过滤

文章区分了两种过滤:FeatureConfig.setEventFilter 决定 feature 接受哪些生命周期上下文,processor 的 setMessageFilter 决定它处理哪些 FeatureMessage,二者作用层不同。若 feature 依赖完整事件流(如 OpenTelemetry 靠完整事件组装 span 层级),安装方的过滤会破坏其逻辑,实现方可在 Config 中覆盖 setEventFilter 直接抛异常禁用过滤。接入前需先想清楚过滤发生在哪一层。

批处理导出可能丢尾批:关闭顺序要检查

文章实测 Kotlin OTel SDK 0.3.0 的 batch processor 在 shutdown 时先关闭 exporter 再取消后台协程,没有先排空待导出队列,本地立即关闭实验中 batch 路径导出 0 条,simple 路径导出 7 条。这说明问题不只是等待时间短,结束时的尾批也可能丢失。该结论仅限此依赖版本,但若选择批处理,应在 SDK shutdown 前有序完成 span 并检查显式刷新结果。

文档与源码有漂移:以版本源码为准

文章多处指出官方页面与 1.3.0 发布源码不一致,例如 LLMCallCompletedEvent 在源码中用单数 response,而官网事件表格写的是复数 responses;测试章节的文档漂移最大。作者提醒,遇到差异应先核对项目依赖版本,再判断是版本差异、文档错误还是上下文省略,不能一概归为“新版 API”,也不能假定升级后差异一定消失。

❓

Q&A

Koog 里自定义 Feature 需要实现哪些接口?怎么注册拦截器?

自定义 Feature 需实现对应 agent 类型的接口(AIAgentGraphFeature、AIAgentFunctionalFeature、AIAgentPlannerFeature),并实现 install(config, pipeline) 方法。在 install 中通过 pipeline.interceptXxx(this) 注册拦截器,公共拦截器声明在 AIAgentPipeline 上,节点级拦截器仅 Graph 管道可用。

Koog 的拦截器能覆盖哪些生命周期?节点级拦截器有什么限制?

公共拦截器覆盖 agent、strategy、LLM 调用、流式、工具调用五组生命周期;节点和子图级拦截器只在 Graph 管道上存在,函数式和 planner 策略没有节点概念,因此没有节点事件。

EventHandler 和 FeatureMessageProcessor 消费事件有什么区别?

EventHandler 通过 handleEvents 注册回调,以函数式方式消费事件上下文;FeatureMessageProcessor 则消费可传输的 Event 对象,需覆写 handleMessage 和 handleClose。Tracing 将事件转为消息交给 processor,EventHandler 回调不直接推送对象到 processor。

装了 Tracing 却看不到输出,可能是什么原因?

Tracing 不注册任何 processor 时只打一条警告,事件照常拦截但不输出到任何地方。需要添加 processor,如 TraceFeatureMessageLogWriter、TraceFeatureMessageFileWriter 或 TraceFeatureMessageRemoteWriter。

OpenTelemetry feature 如何将 Koog 事件转为 span?批处理导出有什么坑?

OpenTelemetry feature 把 Koog 事件翻译成 OTel span,属性遵循 GenAI 语义约定(gen_ai.*),Koog 补充属性放在 koog. 命名空间。批处理路径在 Kotlin OTel SDK 0.3.0 中 shutdown 时先关闭 exporter 再取消协程,可能丢失尾批 span;短程序建议用 simpleSpanProcessor 便于观察。

Koog 的内容审核需要安装 feature 吗?支持哪些模型?

内容审核是模型调用层的独立能力,不需要安装 feature。可通过 LLMClient.moderate 或 PromptExecutor.moderate 调用,返回 ModerationResult(含 isHarmful、categories、violatedCategories)。支持 OpenAIModels.Moderation.Omni 和 OllamaModels.Meta.LLAMA_GUARD_3;DeepSeek 没有审核端点,需另接 OpenAI/Ollama 或自行实现过滤。

Koog 的序列化默认用什么?怎么切换到 Jackson?

默认使用 KotlinxSerializer,可在 AIAgentConfig 中设置 serializer = JacksonSerializer() 切换。两者都实现 JSONSerializer 接口,切换成本仅一行配置,但需注意 Jackson 对 @JvmOverloads、@JsonCreator 等注解的行为差异。

如何用 agents-test 对 Koog agent 做离线测试?

引入 ai.koog:agents-test:1.3.0 和 kotlin("test"),用 getMockExecutor 创建 mock 执行器,通过 mockLLMAnswer 按 onRequestContains 等条件匹配回答,asDefaultResponse 设兜底。可直接作为 promptExecutor 传给 AIAgent 端到端运行,也可用 withTesting { graph { ... } } 或 testGraph 断言图结构。

🏷️

标签

➡️

继续阅读