Files
st-react/docs/flutter参考.md
2026-03-13 21:51:42 +08:00

14 KiB
Raw Blame History

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 + 状态/UIReact


一、变量系统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)

实现建议:

  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

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

推荐拆为多个子步骤(顺序类似原项目):

  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 数据模型

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/o3choice.message.reasoning_content 或自定义扩展字段中解析。
  • Claudethinking 字段解析。
  • Geminithought 或增强字段解析。

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 目标

  • 封装完整一轮对话的流程:
    1. 记录用户消息。
    2. 做摘要/裁剪历史(可选)。
    3. 根据 PromptManager + 世界信息 + 宏/变量 构造 prompt。
    4. 调用 LLM流式/非流式 + reasoning
    5. 写入 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 → 在该消息之前插入对应提示。
  • 对所有文本块跑:
    1. ProcessVariableMacros(...)
    2. ProcessMacros(...)
  • 输出统一形式的 prompt例如 []OpenAIMessage{ {role, content}, ... },交给 LLMProvider 层。

五、提示词管理Prompt Management

5.1 目标

  • 把 prompt 拆成若干「段」,每段可:
    • 单独启用/禁用。
    • 调整顺序。
    • 指定注入深度(在倒数第几条消息前插入)。
  • 与 SillyTavern 的 prompts / prompt_order JSON 兼容。

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 导入:

  1. 建一个 identifier -> PromptSectionType 映射。
  2. 解析 prompts(内容)和 prompt_order(顺序 & enabled生成 PromptSection
  3. 对不在映射表里的 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. Authors Note 深度注入
    // ...

    // 4. 加入当前 chat 消息本身user / assistant
    promptMessages = append(promptMessages, convertChatMessage(msg))
}