457 lines
14 KiB
Markdown
457 lines
14 KiB
Markdown
### NativeTavern 功能设计文档(供 Go + React 集成参考)
|
||
|
||
---
|
||
|
||
## 总览
|
||
|
||
本项目中,这几个模块的作用和依赖关系如下:
|
||
|
||
- **聊天功能(Chat)**:统一负责从「用户消息」到「LLM 调用」再到「写回消息」的完整流程。
|
||
- **提示词管理(Prompt Management)**:把系统提示、人设、世界信息、作者注释、历史等拆成可配置的「段」,决定顺序与注入深度。
|
||
- **宏系统(Macros)**:在构建 prompt 时,根据上下文展开 `{{user}}`、`{{char}}`、`{{time}}`、`{{random}}` 等占位符。
|
||
- **变量系统(Variables)**:提供全局 / 每会话变量 + `{{setvar}}` / `{{getvar}}` 等变量宏,支持状态机、计数等逻辑。
|
||
- **思维链支持(Chain-of-Thought / Reasoning)**:对 o1/o3、Claude、Gemini 等的推理内容单独解析、存储,并在前端单独渲染。
|
||
|
||
Go + React 中建议同样分层:**数据模型(DB) + 领域服务(Go) + 状态/UI(React)**。
|
||
|
||
---
|
||
|
||
## 一、变量系统(Variables System)
|
||
|
||
### 1.1 目标
|
||
|
||
- 支持两类变量:
|
||
- **全局变量**:跨所有聊天共用,用户级。
|
||
- **本地变量**:每个 chat 独立,存到 chat metadata。
|
||
- 通过宏语法在文本中读写变量,如:
|
||
- `{{setvar::score::10}}`
|
||
- `{{incvar::turn}}`
|
||
- `{{getvar::user_name}}`
|
||
|
||
### 1.2 数据模型(示例)
|
||
|
||
后端可参考:
|
||
|
||
```go
|
||
// 全局变量:按 userId 存
|
||
type GlobalVariables map[string]any // name -> value
|
||
|
||
// 本地变量:按 chatId 存
|
||
type LocalVariables map[string]map[string]any // chatId -> (name -> value)
|
||
|
||
type ChatMetadata struct {
|
||
Variables map[string]any `json:"variables,omitempty"`
|
||
// ... 其他 metadata 字段 ...
|
||
}
|
||
```
|
||
|
||
- 全局变量:存 Redis / Postgres JSONB / KV,按 userId 分片。
|
||
- 本地变量:挂在 `chats.metadata.variables` 字段。
|
||
|
||
### 1.3 变量服务接口(Go)
|
||
|
||
```go
|
||
type VariablesService interface {
|
||
// 全局变量
|
||
GetGlobal(userId, name string) any
|
||
SetGlobal(userId, name string, v any) error
|
||
AddGlobal(userId, name string, delta any) (any, error)
|
||
IncGlobal(userId, name string) (any, error)
|
||
DecGlobal(userId, name string) (any, error)
|
||
|
||
// 本地变量(带 chatId)
|
||
GetLocal(userId, chatId, name string) any
|
||
SetLocal(userId, chatId, name string, v any) error
|
||
AddLocal(userId, chatId, name string, delta any) (any, error)
|
||
IncLocal(userId, chatId, name string) (any, error)
|
||
DecLocal(userId, chatId, name string) (any, error)
|
||
|
||
LoadLocalFromMetadata(chatId string, metaVars map[string]any)
|
||
ExportLocalToMetadata(chatId string) map[string]any
|
||
}
|
||
```
|
||
|
||
### 1.4 变量宏语法
|
||
|
||
支持以下语法(与项目一致):
|
||
|
||
- 本地变量:
|
||
- `{{setvar::name::value}}`
|
||
- `{{addvar::name::value}}`
|
||
- `{{incvar::name}}`
|
||
- `{{decvar::name}}`
|
||
- `{{getvar::name}}`
|
||
- 全局变量:
|
||
- `{{setglobalvar::name::value}}`
|
||
- `{{addglobalvar::name::value}}`
|
||
- `{{incglobalvar::name}}`
|
||
- `{{decglobalvar::name}}`
|
||
- `{{getglobalvar::name}}`
|
||
|
||
解析函数示例:
|
||
|
||
```go
|
||
func ProcessVariableMacros(input, userId string, chatId *string, vars VariablesService) (string, error)
|
||
```
|
||
|
||
实现建议:
|
||
|
||
1. 为每种宏写一个正则,比如:
|
||
- `\{\{setvar::([^:]+)::([^}]*)\}\}`
|
||
- `\{\{getvar::([^}]+)\}\}` 等。
|
||
2. 按「写变量 → 读变量」顺序依次 `ReplaceAllStringFunc`:
|
||
- set / add / inc / dec:调用 `VariablesService`,返回空串或新值字符串。
|
||
- get:用变量值替换整段 `{{...}}`。
|
||
|
||
**集成位置**:在构建 prompt、处理用户输入时,先跑一遍 `ProcessVariableMacros`。
|
||
|
||
---
|
||
|
||
## 二、宏系统(Macro System)
|
||
|
||
### 2.1 目标
|
||
|
||
- 使用 `{{...}}` 宏在 prompt 中引入上下文信息,包括:
|
||
- 用户/人设:`{{user}}`、`{{persona}}`、`{{user_description}}`
|
||
- 角色:`{{char}}`、`{{char_description}}`、`{{system_prompt}}`、`{{jailbreak}}`
|
||
- 时间:`{{time}}`、`{{date}}`、`{{weekday}}`
|
||
- 聊天上下文:`{{lastMessage}}`、`{{lastUserMessage}}`、`{{messageCount}}` 等
|
||
- 随机:`{{random}}`、`{{random::min::max}}`、`{{roll::2d6}}`、`{{pick::a::b}}`
|
||
- 元信息:`{{model}}`、`{{provider}}`、`{{idle_duration}}` 等。
|
||
|
||
### 2.2 宏上下文(Go)
|
||
|
||
```go
|
||
type MacroContext struct {
|
||
UserName string
|
||
UserDescription string
|
||
|
||
CharacterName string
|
||
CharacterDescription string
|
||
CharacterSystemPrompt string
|
||
PostHistoryInstructions string
|
||
|
||
LastMessage string
|
||
LastUserMessage string
|
||
LastCharacterMessage string
|
||
MessageCount int
|
||
ChatID string
|
||
|
||
OriginalPrompt string
|
||
CurrentInput string
|
||
ModelName string
|
||
ProviderName string
|
||
IdleDurationMinutes int
|
||
|
||
Now time.Time
|
||
}
|
||
```
|
||
|
||
### 2.3 宏解析函数
|
||
|
||
```go
|
||
func ProcessMacros(text string, ctx MacroContext) string
|
||
```
|
||
|
||
推荐拆为多个子步骤(顺序类似原项目):
|
||
|
||
1. `_processRandomMacros`:
|
||
- `{{random}}`
|
||
- `{{random::min::max}}`
|
||
- `{{roll::NdM}}`
|
||
- `{{pick::opt1::opt2}}`
|
||
- `{{uuid}}`
|
||
2. `_processConditionalMacros`:
|
||
- `{{if::cond::then}}`
|
||
- `{{if::cond::then::else}}`(可以做简单布尔表达式解析)
|
||
3. `_processTimeDateMacros`:
|
||
- `{{time}}` / `{{date}}` / `{{weekday}}` / 自定义时间格式。
|
||
4. `_processCharacterMacros`:
|
||
- `{{char}}`、`{{char_description}}`、`{{system_prompt}}`、`{{post_history_instructions}}` 等。
|
||
5. `_processUserMacros`:
|
||
- `{{user}}` / `{{persona}}` / `{{user_description}}`。
|
||
6. `_processChatMacros`:
|
||
- `{{lastMessage}}` / `{{lastUserMessage}}` / `{{lastCharMessage}}` / `{{messageCount}}` / `{{chatId}}`。
|
||
7. `_processSpecialMacros`:
|
||
- `{{newline}}` / `{{nl}}`、`{{trim}}`、`{{noop}}`
|
||
- `{{original}}`(原始 prompt,用于局部覆盖)
|
||
- `{{input}}`(当前用户输入)
|
||
- `{{model}}` / `{{provider}}`
|
||
- `{{idle_duration}}`。
|
||
|
||
**集成顺序**(在构建 prompt 时):
|
||
|
||
1. 先跑变量宏 `ProcessVariableMacros`(会改变变量状态)。
|
||
2. 再跑文本宏 `ProcessMacros`。
|
||
|
||
---
|
||
|
||
## 三、思维链支持(Chain-of-Thought / Reasoning)
|
||
|
||
### 3.1 目标
|
||
|
||
- 对支持推理的模型(o1/o3、Claude Thinking、Gemini 思考流):
|
||
- 把推理内容单独抓出来(从 `reasoning_content` / `thinking` / `thought` 字段等)。
|
||
- 和正常回复一起存在消息上。
|
||
- 前端给一个可折叠的「思维链/推理」区域。
|
||
|
||
### 3.2 数据模型
|
||
|
||
```go
|
||
type ChatMessage struct {
|
||
ID string
|
||
ChatID string
|
||
Role string // "user" / "assistant" / "system"
|
||
Content string
|
||
Timestamp time.Time
|
||
|
||
Swipes []string
|
||
CurrentSwipeIdx int
|
||
|
||
Reasoning *string // 默认推理文本
|
||
ReasoningSwipes []string // 每个 swipe 的推理
|
||
|
||
// ... 其他字段(attachments、characterId 等)
|
||
}
|
||
```
|
||
|
||
前端便于使用的派生属性(在 React 里实现):
|
||
|
||
- `currentReasoning`:
|
||
- 如果 `ReasoningSwipes` 长度 > `CurrentSwipeIdx` → 用对应项;
|
||
- 否则用 `Reasoning`。
|
||
- `hasReasoning`:
|
||
- `Reasoning` 非空;或 `ReasoningSwipes` 中有任意非空字符串。
|
||
|
||
### 3.3 LLM 接口设计(Go)
|
||
|
||
**非流式:**
|
||
|
||
```go
|
||
type LLMReasoningResponse struct {
|
||
Content string
|
||
Reasoning *string
|
||
}
|
||
|
||
func GenerateWithReasoning(promptCtx PromptContext, cfg LLMConfig) (LLMReasoningResponse, error)
|
||
```
|
||
|
||
**流式:**
|
||
|
||
```go
|
||
type ReasoningChunk struct {
|
||
Content *string
|
||
Reasoning *string
|
||
IsReasoningChunk bool
|
||
}
|
||
|
||
func GenerateStreamWithReasoning(promptCtx PromptContext, cfg LLMConfig) (<-chan ReasoningChunk, error)
|
||
```
|
||
|
||
解析逻辑示例(伪代码):
|
||
|
||
```go
|
||
for chunk := range ch {
|
||
if chunk.IsReasoningChunk && chunk.Reasoning != nil {
|
||
reasoningBuffer.WriteString(*chunk.Reasoning)
|
||
}
|
||
if chunk.Content != nil {
|
||
contentBuffer.WriteString(*chunk.Content)
|
||
}
|
||
// 把当前 buffer 写回当前 assistant 消息,前端即可实时显示
|
||
}
|
||
```
|
||
|
||
- OpenAI o1/o3:从 `choice.message.reasoning_content` 或自定义扩展字段中解析。
|
||
- Claude:从 `thinking` 字段解析。
|
||
- Gemini:从 `thought` 或增强字段解析。
|
||
|
||
### 3.4 前端渲染(React)
|
||
|
||
示例:
|
||
|
||
```tsx
|
||
function MessageView({ message }: { message: ChatMessage }) {
|
||
return (
|
||
<div className="message">
|
||
<div className="content">{renderMarkdownOrHtml(message.content)}</div>
|
||
|
||
{message.hasReasoning && (
|
||
<ReasoningPanel
|
||
label="Thinking"
|
||
content={message.currentReasoning ?? ""}
|
||
/>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
`ReasoningPanel`:
|
||
|
||
- 折叠 / 展开。
|
||
- 文本可复制。
|
||
- UI 上与主内容区明显区分(浅色背景、小字体等)。
|
||
|
||
---
|
||
|
||
## 四、聊天功能(Chat Flow)
|
||
|
||
### 4.1 目标
|
||
|
||
- 封装完整一轮对话的流程:
|
||
1. 记录用户消息。
|
||
2. 做摘要/裁剪历史(可选)。
|
||
3. 根据 PromptManager + 世界信息 + 宏/变量 构造 prompt。
|
||
4. 调用 LLM(流式/非流式 + reasoning)。
|
||
5. 写入 assistant 消息,并更新前端状态。
|
||
|
||
### 4.2 核心流程(伪代码)
|
||
|
||
```go
|
||
func (svc *ChatService) SendMessage(userId, chatId, input string, cfg LLMConfig, atts []ChatAttachment) error {
|
||
chat := repo.GetChat(chatId)
|
||
character := repo.GetCharacter(chat.CharacterID)
|
||
history := repo.ListMessages(chatId)
|
||
|
||
// 1. 写 user 消息
|
||
userMsg := ChatMessage{
|
||
ID: newID(),
|
||
ChatID: chatId,
|
||
Role: "user",
|
||
Content: input,
|
||
Timestamp: time.Now(),
|
||
Swipes: []string{input},
|
||
// Attachments: atts, ...
|
||
}
|
||
repo.AddMessage(userMsg)
|
||
|
||
// 2. 可选:基于 token 限制做摘要或丢弃最老历史
|
||
svc.checkAndSummarize(chat, history, cfg)
|
||
|
||
// 3. 构建 prompt 上下文
|
||
promptCtx := svc.BuildPromptContext(userId, chat, character, history, input)
|
||
|
||
// 4. LLM 调用
|
||
if cfg.Stream {
|
||
return svc.streamAssistantResponse(chatId, promptCtx, cfg)
|
||
}
|
||
return svc.oneShotAssistantResponse(chatId, promptCtx, cfg)
|
||
}
|
||
```
|
||
|
||
### 4.3 BuildPromptContext 的职责
|
||
|
||
- 从 PromptManager 获取启用的 PromptSection(排序后)。
|
||
- 从世界信息系统取匹配条目(按关键词、优先级、位置)。
|
||
- 按「深度注入」规则遍历历史消息,决定在每条消息前插入哪些 PromptSection / 世界信息 / 作者注释:
|
||
- 定义 `depthFromEnd = total-1 - i`。
|
||
- 若 `section.InjectionDepth == depthFromEnd` → 在该消息之前插入对应提示。
|
||
- 对所有文本块跑:
|
||
1. `ProcessVariableMacros(...)`
|
||
2. `ProcessMacros(...)`
|
||
- 输出统一形式的 prompt,例如 `[]OpenAIMessage{ {role, content}, ... }`,交给 LLMProvider 层。
|
||
|
||
---
|
||
|
||
## 五、提示词管理(Prompt Management)
|
||
|
||
### 5.1 目标
|
||
|
||
- 把 prompt 拆成若干「段」,每段可:
|
||
- 单独启用/禁用。
|
||
- 调整顺序。
|
||
- 指定注入深度(在倒数第几条消息前插入)。
|
||
- 与 SillyTavern 的 `prompts` / `prompt_order` JSON 兼容。
|
||
|
||
### 5.2 数据模型
|
||
|
||
```go
|
||
type PromptSectionType string
|
||
|
||
const (
|
||
SectionSystemPrompt PromptSectionType = "systemPrompt"
|
||
SectionPersona PromptSectionType = "persona"
|
||
SectionCharacterDescription PromptSectionType = "characterDescription"
|
||
SectionCharacterPersonality PromptSectionType = "characterPersonality"
|
||
SectionCharacterScenario PromptSectionType = "characterScenario"
|
||
SectionExampleMessages PromptSectionType = "exampleMessages"
|
||
SectionWorldInfo PromptSectionType = "worldInfo"
|
||
SectionWorldInfoAfter PromptSectionType = "worldInfoAfter"
|
||
SectionAuthorNote PromptSectionType = "authorNote"
|
||
SectionPostHistoryInstr PromptSectionType = "postHistoryInstructions"
|
||
SectionNsfw PromptSectionType = "nsfw"
|
||
SectionChatHistory PromptSectionType = "chatHistory"
|
||
SectionEnhanceDefinitions PromptSectionType = "enhanceDefinitions"
|
||
SectionCustom PromptSectionType = "custom"
|
||
)
|
||
|
||
type PromptSection struct {
|
||
Type PromptSectionType
|
||
Name string
|
||
Enabled bool
|
||
Order int
|
||
|
||
Content *string
|
||
Identifier *string // ST 中的 identifier
|
||
Role *string // "system" / "user" / "assistant"
|
||
InjectionPosition *int
|
||
InjectionDepth *int
|
||
}
|
||
```
|
||
|
||
### 5.3 PromptManager 服务
|
||
|
||
```go
|
||
type PromptManager struct {
|
||
Sections []PromptSection
|
||
}
|
||
|
||
func (pm *PromptManager) EnabledSections() []PromptSection {
|
||
// 过滤 Enabled,并按 Order 排好
|
||
}
|
||
```
|
||
|
||
从 SillyTavern 导入:
|
||
|
||
1. 建一个 `identifier -> PromptSectionType` 映射。
|
||
2. 解析 `prompts`(内容)和 `prompt_order`(顺序 & enabled),生成 `PromptSection`。
|
||
3. 对不在映射表里的 identifier,统一归类为 `SectionCustom`。
|
||
|
||
### 5.4 深度注入逻辑
|
||
|
||
在构建 prompt 时,遍历历史消息 `messages`:
|
||
|
||
```go
|
||
N := len(messages)
|
||
for i, msg := range messages {
|
||
depthFromEnd := N - 1 - i
|
||
|
||
// 1. 深度注入世界信息
|
||
for _, entry := range depthWorldInfo {
|
||
if entry.Depth == depthFromEnd {
|
||
systemMsg := buildWorldInfoMessage(entry)
|
||
promptMessages = append(promptMessages, systemMsg)
|
||
}
|
||
}
|
||
|
||
// 2. 深度注入 PromptSection
|
||
for _, sec := range depthBasedSections {
|
||
if sec.InjectionDepth != nil && *sec.InjectionDepth == depthFromEnd {
|
||
secMsgs := buildSectionMessages(sec, ctx)
|
||
promptMessages = append(promptMessages, secMsgs...)
|
||
}
|
||
}
|
||
|
||
// 3. Author’s Note 深度注入
|
||
// ...
|
||
|
||
// 4. 加入当前 chat 消息本身(user / assistant)
|
||
promptMessages = append(promptMessages, convertChatMessage(msg))
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
|