灵感来源于 SCP: 4K 中黑洞纪元的舰载人工智能 -- 未奇
一个精简、易维护的 Kotlin Agent 框架,对接 OpenAI 通用协议(兼容 OpenAI / DeepSeek / one-api / 本地推理服务等)。
底层用 aallam/openai-kotlin 作 API 客户端,agent 主循环、模式、权限、工具注册全部自写,方便按需改造。
| 模块 | 功能分类 | 功能点 | 说明 |
|---|---|---|---|
| vicky-core | Agent 运行时 | 多模式推理 | 内置 SILENT / VERBOSE / CHAT,支持继承 AgentMode 自定义模式 |
| 主循环引擎 | maxSteps 限制的多轮推理,支持工具调用链与步数耗尽自动汇报 |
||
| OpenAI 协议客户端 | 基于 openai-kotlin,兼容 OpenAI / DeepSeek / one-api / 本地推理,支持流式 | ||
| 消息 IO 抽象 | InboundMessage / OutboundMessage / MessageSink 解耦输入输出 |
||
| 权限系统 | ToolAuthorizer 框架层按 userId + toolName 鉴权 |
||
| 工具系统 | 双输出调用 | ToolResult(toAgent, userReply) 分别喂回 Agent 与推送给用户 |
|
| 运行时工具管理 | registerTool / unregisterTool,下一轮自动同步到 system prompt |
||
| 上下文注入 | ToolContext 提供会话历史、工具注册表、消息缓冲区等运行时访问 |
||
| 步数耗尽保护 | maxSteps 用尽时自动追加系统提示,让模型整理现状并汇报 |
||
| 上下文管理 | 上下文压缩 | ContextCompactor 超 token 上限时自动调用 LLM 生成摘要 |
|
| 安全防护 | # Security 段始终固定拼接,不可被 agentMd 关闭,防御提示词注入/反取 |
||
| system prompt 拼接 | 固定顺序:agentMd → Security → Memory → Output rules → Available tools |
||
| 记忆与 RAG | 语义记忆(长时) | 对话知识向量化持久化,每轮自动 recall 注入 system prompt |
|
| 记忆蒸馏 | 定时任务(默认每日 02:00)将原始对话通过 LLM 压缩为精炼记忆 | ||
| 双层存储 | RawMemory 保留原始对话用于回溯,Memory 存储蒸馏后精炼记忆 |
||
| 语义搜索 | 基于向量相似度检索记忆,支持按用户过滤 | ||
| 文件语义搜索 | 自动索引 | Agent 启动时增量索引文本文件,只处理新增/修改 | |
| 段落分块 | 文件按段落分块后向量化存储 | ||
| 语义检索 | 通过向量相似度搜索已索引文件内容 | ||
| 技能系统 | SKILL.md 定义 | Frontmatter(name / description)+ Markdown 内容描述技能 | |
| 分组机制 | 同一目录(含 group.md)的技能折叠展示,按需展开 |
||
| 动态加载 | 支持按技能名或分组名加载全文,支持 enable / disable / delete 生命周期 | ||
| vicky-ksp | 注解处理 | 编译期代码生成 | 扫描 @VickyTool 生成 Tool 子类与 ToolRegistry |
| 参数适配 | @ToolParam(required = false) 或 Kotlin 默认值 → 可选参数 |
||
| 隐式注入 | userId: String 自动接收调用者 ID,不进入 JSON Schema |
||
| 上下文注入 | ToolContext 参数自动注入运行时上下文 |
||
| 返回值包装 | 返回 ToolResult 直接使用,其他类型自动 toString() 包装 |
||
| vicky-script | 脚本引擎 | TS/JS 执行 | Rhino JS 引擎 + 内嵌 TypeScript 4.9.5 编译器 + Promise polyfill |
| 三种脚本模式 | 顶层直接执行 / 导出 execute 作为工具 / 导出 onLoad+onUnload 插件模式 |
||
| 自动加载 | config/scripts/ 下 .ts 文件启动时自动加载,无 name 时以文件名命名 |
||
| Kotlin 互操作 | Object 单例自动注入 JS 全局,普通类注入为构造函数代理 | ||
| 协程支持 | coroutine.launch() 启动后台任务,脚本卸载时自动取消 |
||
| 主动消息 | ctx.sendGroupMessage() / sendMessage() / setTimer() |
||
| 生命周期安全 | onLoad 10s 超时、循环依赖检测、onUnload 异常隔离 |
||
| 运行时 API 技能 | 扫描类自动生成 API 文档技能,归入 runtime-api 分组 |
||
| 通用 | 配置 | AgentConfig | 支持 model / apiKey / baseUrl / maxSteps / temperature / mode / debug / think / streaming 等参数 |
项目由四个独立模块 + 一个根应用组成:
src/main/
├── vicky-core/ # 核心框架(Maven: io.github.zenthxsin:vicky-core)
│ └── kotlin/org/example/vicky/
│ ├── agent/ # Agent 基类、配置、模式、管理器
│ ├── annotations/ # @VickyTool @ToolParam @ToolGroup 注解
│ ├── context/ # ContextManager 接口
│ ├── io/ # InboundMessage / OutboundMessage / MessageSink
│ ├── llm/ # OpenAI 客户端工厂
│ ├── skill/ # Skill 数据模型 + SkillManager
│ └── tool/ # Tool 抽象、ToolRegistry、ToolContext、ToolResult、ToolAuthorizer
│
├── vicky-ksp/ # KSP 注解处理器(Maven: io.github.zenthxsin:vicky-ksp)
│ └── src/main/kotlin/org/example/vicky/ksp/
│ ├── VickyToolProcessor.kt # @VickyTool 注解处理 → 生成 Tool 子类
│ └── VickyToolProcessorProvider.kt
│
├── vicky-script/ # 动态脚本模块(Maven: io.github.zenthxsin:vicky-script)
│ ├── kotlin/org/example/vicky/script/
│ │ ├── ScriptEngine.kt # Rhino JS 引擎 + TS 编译 + Promise polyfill
│ │ ├── ScriptManager.kt # 脚本生命周期 + 热加载
│ │ ├── ScriptToolBridge.kt # JS Tool → vicky Tool 适配
│ │ ├── ClassAutoRegistry.kt # classpath 类自动注入到 JS 全局
│ │ └── ScriptConfig.kt # 配置模型
│ └── resources/
│ └── typescript.js # 内嵌 TypeScript 4.9.5 编译器
│
├── vicky-vibe/ # 单轮自适应编排 / Code CLI(当前仓库内模块)
│ └── kotlin/org/example/vicky/vibe/
│ ├── agent/ # VibeAgent + 专用 ContextManager
│ ├── engine/ # 单轮 message pipeline
│ ├── orchestrator/ # VibeOrchestrator / system prompt / 结果适配
│ ├── status/ # 状态面板、观察者、快照
│ ├── task/ # 任务图与任务状态
│ ├── tool/ # tool use queue / tool use result
│ └── turn/ # turn request / runner / result
│
└── kotlin/org/example/vicky/ # 根应用(OneBot 机器人 + 内置工具实现)
├── agent/EmbeddingConfig.kt
├── buffer/MessageBuffer.kt
├── channel/onebot/ # OneBot WebSocket + Mirai 工具集(注解式)
├── config/ConfigManager.kt
├── context/ # ContextBuilder / ContextCompactor / ConversationStore / DefaultContextManager
├── examples/ # ConsoleMain / StreamDumpMain / MindustryMITToolImpl
├── file/FileIndexService.kt
├── llm/ # EmbeddingClient / OpenAiEmbeddingClient / EmbeddingClientFactory
├── logging/SilentMiraiLoggerFactory.kt
├── memory/ # Memory / RawMemory / MemoryStore / QdrantMemoryStore / Distiller / DistillationScheduler
├── skill/SkillFrontmatterParser.kt / SkillLoader.kt
├── tool/builtin/ # 注解式内置工具(BuiltinToolImpl / InvokeSkillTool / ManageSkillsTool / ToolManagementTool)
├── tool/file/FileDownloader.kt
└── vector/VectorStore.kt / QdrantVectorStore.kt
| 模式 | toolsEnabled |
emitAgentText |
工具 userReply |
|---|---|---|---|
SILENT |
✅ | ❌ | ✅ |
VERBOSE |
✅ | ✅ | ✅ |
CHAT |
❌ | ✅ | —(无工具) |
模式是可继承的抽象类 AgentMode,可自定义:
object ReviewMode : AgentMode() {
override val name = "REVIEW"
override val toolsEnabled = true
override val emitAgentText = false
override val instructions = "You are reviewing code. Only speak to the user through tools."
}- 工具调用双输出:每个工具返回
ToolResult(toAgent, userReply),toAgent喂回 agent 继续推理,userReply(可选)实时推送给 user。 - 步数耗尽汇报:
maxSteps用尽时追加系统提示,让模型整理已有信息并向用户汇报。 - 权限系统:
ToolAuthorizer.allow(userId, toolName)框架层鉴权。 - 运行时增删工具:
registerTool/unregisterTool,下一轮自动反映到 system prompt。
使用 @VickyTool、@ToolParam、@ToolGroup 注解定义工具,KSP 自动生成 Tool 子类和 ToolRegistry:
@ToolGroup(name = "my_tools")
object MyTools {
@VickyTool(name = "ping", description = "Ping a host and measure latency.")
suspend fun ping(
@ToolParam(description = "Host to ping.") host: String,
@ToolParam(description = "Timeout in ms.", required = false) timeout: Int = 5000,
): ToolResult {
val result = doPing(host, timeout)
return ToolResult(toAgent = result, userReply = result)
}
// ToolContext 注入:访问会话历史、工具注册表、消息缓冲区等
@VickyTool(name = "clear", description = "Clear conversation context.")
fun clear(ctx: ToolContext): ToolResult {
ctx.contextManager.clear(ctx.conversationId)
return ToolResult(toAgent = "done", userReply = "上下文已清除。")
}
// userId 注入:自动接收调用者 ID,不进入参数 Schema
@VickyTool(name = "whoami", description = "Get caller user ID.")
fun whoami(userId: String): ToolResult =
ToolResult(toAgent = "userId=$userId")
}特性:
@ToolParam(required = false)或 Kotlin 默认值 → 可选参数userId: String参数 → 自动注入调用者 ID,不进入 JSON SchemaToolContext参数 → 注入运行时上下文(会话历史、工具注册表、消息缓冲区等)- 返回值是
ToolResult直接使用,否则自动toString()包装 - 生成代码位于
org.example.vicky.generated包
技能是给 LLM 看的操作指南,通过 SKILL.md 文件定义。支持分组:同一分组的技能在 system prompt 中折叠展示,Agent 按需展开。
config/skills/
├── runtime-api/ ← 分组目录(含 group.md)
│ ├── group.md ← 分组介绍
│ ├── AgentManager/
│ │ └── SKILL.md
│ └── ConfigManager/
│ └── SKILL.md
├── code-review/ ← 普通技能目录
│ └── SKILL.md
└── translate/
└── SKILL.md
SKILL.md 格式:
---
name: code-review
description: 代码审查技能
---
你是一个代码审查专家。当用户请求代码审查时...group.md 格式(分组介绍):
---
name: runtime-api
description: Vicky 运行时类和对象的 API 文档
---
此分组包含所有自动注入的运行时类。分组展示逻辑:
- 无分组技能:system prompt 中直接显示
name: description - 有分组技能:只显示分组名 + 描述,Agent 调用
invoke_skill(name="<group>")查看组内技能
相关工具:
invoke_skill— 加载技能全文;传入分组名则返回组内所有技能manage_skills— list / enable / disable / delete 技能,支持按group过滤
- 长时记忆(RAG):对话知识持久化到 Qdrant 向量数据库,每轮自动 recall 注入 system prompt。
- 记忆蒸馏:定时(默认每天凌晨 2:00)将原始对话通过 LLM 压缩为精炼记忆。
- 双层存储:原始对话(RawMemory)+ 蒸馏记忆(Memory),原始信息保留用于回溯。
- 语义搜索:基于向量相似度的记忆检索,支持按用户过滤。
相关工具:
memory_store— 手动存储重要信息到长期记忆memory_search— 语义搜索记忆memory_distill— 手动触发记忆蒸馏
- 自动索引:Agent 启动时自动索引根目录下的文本文件(增量索引,只处理新增/修改的文件)。
- 分块存储:文件按段落分块,向量化后存入 Qdrant。
- 语义搜索:
file_search工具支持按语义搜索已索引的文件。
相关工具:
file_search— 语义搜索已索引的文件file_index— 手动触发后台文件索引
TypeScript 脚本的编译、执行和 Tool 桥接。config/scripts/ 下的 .ts 文件启动时自动加载。
特性:
- 零配置启动:脚本无需导出
name/execute/onLoad,顶层代码直接运行 - 自动命名:不导出
name时用文件名(hello.ts→hello) - TypeScript 编译:内嵌 TypeScript 4.9.5 编译器,
.ts→.js - Kotlin object 自动注入:
AgentManager、ConfigManager、MiraiToolImpl等单例脚本中直接使用 - 普通类自动注入:其他类注入为构造函数代理,
new ClassName(...)创建实例 - 运行时 API 技能自动生成:扫描时自动为每个类生成 API 文档技能,归入
runtime-api分组 - 真协程:
coroutine.launch()启动后台异步任务,脚本卸载时自动取消 - ctx 主动发消息:
ctx.sendGroupMessage()/ctx.sendMessage()/ctx.setTimer() - 生命周期钩子:可导出
onLoad()/onUnload()函数 - 生命周期安全:onLoad 超时(10s)、循环依赖检测、onUnload 异常不阻止卸载
三种模式:
// 1. 顶层直接执行(不导出任何东西)
var bot = MiraiToolImpl.bot;
java.lang.System.out.println("Bot: " + bot.getNick());
// 2. 定义工具(导出 execute)
var name = "hello";
var description = "打招呼";
async function execute(ctx, args) {
return { toAgent: "hi", userReply: "你好!" };
}
// 3. 插件模式(导出 onLoad/onUnload)
var name = "my_plugin";
function onLoad() { /* 初始化 */ }
function onUnload() { /* 清理 */ }ctx API(仅在 execute 内可用):
ctx.userId/ctx.conversationId/ctx.groupId— 只读属性ctx.sendGroupMessage(groupId, text)— 发群消息ctx.sendMessage(targetId, text)— 发私聊消息ctx.setTimer(intervalMs, callback)— 定时器,返回timer.cancel()
协程:
coroutine.launch(function(co) {
co.delay(5000); // 非阻塞延迟
ctx.sendGroupMessage("123", "5秒后"); // execute 内可用 ctx
});运行时访问(无需 import):
var agents = AgentManager.all();
var config = ConfigManager.loadOrCreate();
var bot = MiraiToolImpl.bot;
var f = new File("./config/config.json");
var content = Files.readString(f.toPath());根应用提供 manage_scripts 工具,Agent 可按需加载/卸载/重载脚本。启动时自动加载所有 .ts 文件。
vicky-vibe 提供一个更接近 Claude Code / Kode 风格的单轮自适应编排内核,不再依赖固定多阶段 JSON 输出流水线,而是围绕一轮 turn 内的 message history、tool calling、tool result 回灌和收尾总结来运行。
当前能力:
- 单轮 adaptive loop:一次请求内动态决定“思考 → 调工具 → 基于结果继续推进 → 收尾”。
- 连续对话:
conversationId驱动上下文复用,可关闭resetContextEachTurn形成持续会话。 - 可嵌入 orchestrator API:外部项目可直接复用
VibeOrchestrator、VibeTurnRunner、VibeSystemPromptBuilder。 - 过程消息流:通过
MessageSink输出AgentReply/ToolReply/Debug/Think/TokenUsage,可自行渲染成终端 UI、聊天 UI 或状态栏。 - 任务流:工具调用会写入
TaskGraph,便于在 CLI 或宿主应用展示进行中的步骤。
核心入口:
org.example.vicky.vibe.orchestrator.VibeOrchestratororg.example.vicky.vibe.turn.VibeTurnRunnerorg.example.vicky.vibe.orchestrator.VibeSystemPromptBuilderorg.example.vicky.examples.VibeCodeCliMain
适合的使用方式:
- 在现有机器人/服务里嵌入一个 code agent 回合执行器
- 复用 Vicky 的工具系统做交互式终端或 IDE 助手
- 在别的 JVM 项目里直接接入连续对话 + 工具调用 + 状态流
仓库内提供 org.example.vicky.examples.VibeCodeCliMain 作为交互式 code CLI 示例,直接复用当前 config.json 与 agentMd 配置。
特性:
- 多行输入,空行发送
/help、/clear、/status、/exit命令- 持续对话(默认复用同一
conversationId) - 实时显示思考、工具、任务和 token 用量消息流
建议在 IDE 中直接运行 VibeCodeCliMain.main();如果用 Gradle 运行,也可以把该入口配置为应用主类后启动。
- 上下文压缩:
ContextCompactor在会话历史超出 token 上限时自动调用 LLM 生成摘要。 - 内置安全防护:始终拼接、无法被
agentMd关闭,抵御提示词反取 / 注入 / 调试输出。
debug = true:框架运行日志(每步推理、工具调用、上下文清除等)。think = true:agent 中间思考文本。
两者都通过 MessageSink 推送(OutboundMessage.Debug / Think),由外部决定如何展示。
dependencies {
implementation("io.github.zenthxsin:vicky-core:xxx")
// 可选:KSP 注解处理器
ksp("io.github.zenthxsin:vicky-ksp:xxx")
// 可选:动态脚本支持
implementation("io.github.zenthxsin:vicky-script:xxx")
}class MyAgent(config: AgentConfig) : Agent(config) {
override val contextManager = DefaultContextManager(
store = ConversationStore(),
builder = ContextBuilder(config.agentMd),
compactor = ContextCompactor(config, OpenAiClientFactory.create(config)),
)
override val sink = MessageSink { out ->
when (out) {
is OutboundMessage.AgentReply -> println("[agent] ${out.content}")
is OutboundMessage.ToolReply -> println("[tool] ${out.content}")
is OutboundMessage.Debug -> println("[debug] ${out.content}")
is OutboundMessage.Think -> println("[think] ${out.content}")
}
}
override val authorizer = ToolAuthorizer { userId, toolName ->
if (toolName == "shutdown") userId == "admin" else true
}
}val agent = MyAgent(
AgentConfig(
model = ModelId("deepseek-v4-flash"),
apiKey = "sk-...",
baseUrl = "http://192.168.0.108:3000/v1",
mode = AgentMode.SILENT,
maxSteps = 6,
agentMd = "你是 Vicky,一个简洁的助手。",
debug = false,
builtinTools = true,
)
)
agent.receive(InboundMessage("user1", "ping 192.168.0.108"))Android 应用可以直接依赖现有的 vicky-core 和 vicky-script 坐标,不需要依赖根应用或
vicky-vibe:
dependencies {
implementation("io.github.zenthxsin:vicky-core:<version>")
implementation("io.github.zenthxsin:vicky-script:<version>")
ksp("io.github.zenthxsin:vicky-ksp:<version>")
}库产出 Java 17 字节码。脚本模块建议 Android API 26 及以上,并由宿主开启网络权限、管理 协程和后台生命周期。脚本由宿主以名称和内容加载,不扫描或监听 Android 文件目录:
val bridge = ScriptManager.loadScript(
scriptName = "hello",
scriptContent = scriptText,
)
// 需要自动注册脚本导出的 Tool 时:
ScriptManager.loadAndRegister("hello", scriptText, agent.tools)Android/Dex 无法使用 JVM classpath 扫描。需要暴露给脚本的宿主类应显式注册:
ClassAutoRegistry.registerAll(MyApi::class.java, MyModel::class.java)会话持久化可使用跨 JVM/Android 的 JsonSessionStore(filesDir);JDBC
SqliteSessionStore 保留给桌面 JVM。HTTP MCP 可用,stdio MCP 在 Android 上不支持。
Rhino extend() 的 ART 动态类生成仍在适配中,Android 脚本暂时应使用显式注册的具体类。
根应用使用 config/config.json 配置文件,参见 ROOT-MODULE.md。
| 参数 | 默认值 | 说明 |
|---|---|---|
model |
— | LLM 模型 ID |
apiKey |
— | API 密钥 |
baseUrl |
null | OpenAI 兼容端点,null = 官方 |
maxSteps |
8 | 单次 receive 最大推理轮数 |
maxMemoryRounds |
50 | 最多保留多少轮用户消息 |
maxContextLength |
0 | 上下文 token 上限,0 = 不限制 |
mode |
SILENT | 运行模式:SILENT / VERBOSE / CHAT |
temperature |
null | 采样温度 |
agentMd |
"You are a helpful assistant." | system prompt 人设/指令 |
debug |
false | 框架运行日志 |
think |
false | Agent 中间思考文本 |
streaming |
true | 是否使用流式请求 |
builtinTools |
true | 是否自动注册内置工具 |
agentMd—— 人设/指令# Security—— 内置安全防护(固定拼接,不可关闭)# Memory—— 长期记忆 recall(如果启用)# Output rules—— 当前模式说明# Available tools—— 已注册工具名 + 简介
1. recall 蒸馏记忆 → 注入 system prompt
2. 用户消息追加到 history
3. for step in 0 until maxSteps:
resp = chat.completion(history + tools)
if resp.toolCalls 非空:
执行工具 → result
history += assistantToolCall + toolMessage(result.toAgent)
result.userReply?.let { emit(ToolReply) }
if result.endTurn → return
continue
else:
emit(AgentReply)
return
4. 步数耗尽:注入系统提示,让模型整理现状并向用户汇报
5. finally: 保存原始记忆到 Qdrant
./gradlew test运行脚本模块测试:
./gradlew :src:main:vicky-script:testApache License 2.0
