14 KiB
14 KiB
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 数据模型(示例)
后端可参考:
// 全局变量:按 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)
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}}
解析函数示例:
func ProcessVariableMacros(input, userId string, chatId *string, vars VariablesService) (string, error)
实现建议:
- 为每种宏写一个正则,比如:
\{\{setvar::([^:]+)::([^}]*)\}\}\{\{getvar::([^}]+)\}\}等。
- 按「写变量 → 读变量」顺序依次
ReplaceAllStringFunc:- set / add / inc / dec:调用
VariablesService,返回空串或新值字符串。 - get:用变量值替换整段
{{...}}。
- set / add / inc / dec:调用
集成位置:在构建 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)
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 宏解析函数
func ProcessMacros(text string, ctx MacroContext) string
推荐拆为多个子步骤(顺序类似原项目):
_processRandomMacros:{{random}}{{random::min::max}}{{roll::NdM}}{{pick::opt1::opt2}}{{uuid}}
_processConditionalMacros:{{if::cond::then}}{{if::cond::then::else}}(可以做简单布尔表达式解析)
_processTimeDateMacros:{{time}}/{{date}}/{{weekday}}/ 自定义时间格式。
_processCharacterMacros:{{char}}、{{char_description}}、{{system_prompt}}、{{post_history_instructions}}等。
_processUserMacros:{{user}}/{{persona}}/{{user_description}}。
_processChatMacros:{{lastMessage}}/{{lastUserMessage}}/{{lastCharMessage}}/{{messageCount}}/{{chatId}}。
_processSpecialMacros:{{newline}}/{{nl}}、{{trim}}、{{noop}}{{original}}(原始 prompt,用于局部覆盖){{input}}(当前用户输入){{model}}/{{provider}}{{idle_duration}}。
集成顺序(在构建 prompt 时):
- 先跑变量宏
ProcessVariableMacros(会改变变量状态)。 - 再跑文本宏
ProcessMacros。
三、思维链支持(Chain-of-Thought / Reasoning)
3.1 目标
- 对支持推理的模型(o1/o3、Claude Thinking、Gemini 思考流):
- 把推理内容单独抓出来(从
reasoning_content/thinking/thought字段等)。 - 和正常回复一起存在消息上。
- 前端给一个可折叠的「思维链/推理」区域。
- 把推理内容单独抓出来(从
3.2 数据模型
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)
非流式:
type LLMReasoningResponse struct {
Content string
Reasoning *string
}
func GenerateWithReasoning(promptCtx PromptContext, cfg LLMConfig) (LLMReasoningResponse, error)
流式:
type ReasoningChunk struct {
Content *string
Reasoning *string
IsReasoningChunk bool
}
func GenerateStreamWithReasoning(promptCtx PromptContext, cfg LLMConfig) (<-chan ReasoningChunk, error)
解析逻辑示例(伪代码):
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)
示例:
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 目标
- 封装完整一轮对话的流程:
- 记录用户消息。
- 做摘要/裁剪历史(可选)。
- 根据 PromptManager + 世界信息 + 宏/变量 构造 prompt。
- 调用 LLM(流式/非流式 + reasoning)。
- 写入 assistant 消息,并更新前端状态。
4.2 核心流程(伪代码)
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→ 在该消息之前插入对应提示。
- 定义
- 对所有文本块跑:
ProcessVariableMacros(...)ProcessMacros(...)
- 输出统一形式的 prompt,例如
[]OpenAIMessage{ {role, content}, ... },交给 LLMProvider 层。
五、提示词管理(Prompt Management)
5.1 目标
- 把 prompt 拆成若干「段」,每段可:
- 单独启用/禁用。
- 调整顺序。
- 指定注入深度(在倒数第几条消息前插入)。
- 与 SillyTavern 的
prompts/prompt_orderJSON 兼容。
5.2 数据模型
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 服务
type PromptManager struct {
Sections []PromptSection
}
func (pm *PromptManager) EnabledSections() []PromptSection {
// 过滤 Enabled,并按 Order 排好
}
从 SillyTavern 导入:
- 建一个
identifier -> PromptSectionType映射。 - 解析
prompts(内容)和prompt_order(顺序 & enabled),生成PromptSection。 - 对不在映射表里的 identifier,统一归类为
SectionCustom。
5.4 深度注入逻辑
在构建 prompt 时,遍历历史消息 messages:
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))
}