Files
HxClaw/taolun.md
titor 5d9498f687
Some checks failed
Release / build (push) Failing after 4m28s
feat: 记忆体系统 v0.3.0 完成
## 核心功能
- 双记忆系统合并:picoclaw MEMORY.md + hxclaw 会话摘要
- 独立上下文系统:不依赖 picoclaw session
- 向量检索:硅基流动 BGE-M3 API
- 三重检测:关键词/向量相似度/命令

## 数据库
- libSQL (TursoDB) 存储
- sessions + chats 表设计
- 向量存储使用 binary 编码

## 查询场景
- RecallHistory: 查询所有会话摘要
- RecallTopic: 按话题向量检索
- RecallSession: 指定会话详情
- RecallWithinSession: 会话内检索

## 导出
- MongoDB 风格:~/.config/hxclaw/export-data.json
- chats 嵌套在 sessions 下
- 增量导出,同 session 累加

## UI 优化
- 合并状态显示(耗时 · 状态 · 消息数)
- 颜色设计:金色图标 + 暗绿色/暗红色状态

## 配置项
- memory.recall: keywords, auto_recall, similarity_threshold
- memory.vector: max_search_results
- memory.auto_export
2026-04-27 06:16:19 +08:00

14 KiB
Raw Blame History

hxclaw 讨论记录

知识点汇总

1. hxclaw 是什么?

  • hxclaw 是基于 picoclaw 的 CLI 增强工具
  • 提供流式输出(替代原有的批量输出)
  • 提供 Markdown 终端渲染(提升阅读体验)
  • 定位: picoclaw 的变体,面向需要更好终端交互体验的用户

2. 为什么采用独立二进制而非插件?

  • picoclaw 目前没有正式的插件系统
  • 工具扩展通过 ToolRegistry 注册(如 spi.go, i2c.go
  • 技能通过 SKILL.md 文件加载
  • MCP 通过配置连接外部服务器
  • 最小侵入方式:独立二进制 + 复用核心库

3. 如何复用 picoclaw 功能?

  • 使用 Go 的 replace 机制在 go.mod 中声明依赖
  • 开发时 replace 指向本地 picoclaw 目录
  • 发布时 replace 指向 GitHub 具体版本(如 v0.2.4
  • hxclaw 只需导入pkg/agent, pkg/providers, pkg/config, pkg/bus

4. hxclaw 架构设计

hxclaw/
├── cmd/hxclaw/     # CLI 入口(自己实现)
├── go.mod          # 依赖配置
└── pkg/            # 空目录,全量复用 picoclaw
  • 不依赖pkg/channels不需要消息通道、pkg/gateway不需要 HTTP 服务、web/(不需要网页)
  • 依赖pkg/agent核心逻辑、pkg/providersLLM 调用、pkg/config配置加载

5. 流式输出原理

  • picoclaw 的 providers 已支持 StreamingProvider 接口
  • 接口定义ChatStream(ctx, messages, tools, model, options, onChunk)
  • onChunk 是回调函数,每个 token 生成时调用
  • CLI 层需要判断 provider 是否实现 StreamingProvider然后选择调用

6. Markdown 终端渲染

  • 使用 charmbracelet 家族库
  • glamourMarkdown 渲染(自带代码高亮)
  • lipgloss终端样式
  • 流程Markdown → ANSI 转义序列 → 终端显示

7. 部署方式

  • 独立二进制 hxclaw与 picoclaw 二进制共存于同一目录
  • 用户使用 hxclaw 命令调用增强版 CLI
  • 配置文件复用 picoclaw 的 config.json位于 ~/.picoclaw/config.json

8. 版本同步策略

  • 关键版本跟进(功能大版本更新时)
  • 不需要每次 picoclaw 升级都同步
  • 依赖版本在 go.mod 中声明,更新时修改 replace 目标版本即可

9. AgentRegistry 的正确使用方式

  • AgentRegistry 负责管理多个 Agent 实例
  • GetDefaultAgent() 获取默认的 Agent 实例
  • AgentInstance 包含:
    • Provider - LLM 提供者
    • ContextBuilder - 消息构建器
    • Tools - 工具注册表
    • Model - 模型名称
  • 注意:AgentRegistry 没有 BuildMessages 方法,该方法属于 ContextBuilder

10. ToolDefinitions 的获取方式

  • 通过 agentInstance.Tools.ToProviderDefs() 获取
  • 返回 []providers.ToolDefinition 格式
  • 该方法将工具注册表中的工具转换为 provider 可用的格式

11. 流式输出的实现问题与解决方案

问题 1onChunk 回调接收累积文本

picoclaw 的 StreamingProvider 接口定义:

onChunk func(accumulated string)

注释明确说明:"onChunk receives the accumulated text so far (not individual deltas)"。

这意味着每次回调时参数是完整的累积文本(如 "你好" → "你好!再次" → "你好!再次见到"),而不是增量。

问题 2直接打印导致重复输出

如果直接打印 token

func(token string) {
    fmt.Print(token)  // 打印累积文本!
}

会导致输出:

你好
你好!再次
你好!再次见到
...

解决方案 1跟踪已打印长度

使用 printedLen 跟踪已打印的字符位置,只打印新增部分:

var printedLen int
func(accumulated string) {
    if len(accumulated) > printedLen {
        fmt.Print(accumulated[printedLen:])
        printedLen = len(accumulated)
    }
}

问题 3尝试使用 uilive 库

尝试使用 github.com/gosuri/uilive 库实现同行流动效果,但发现该库会覆盖每一行,只显示最后一行内容,不符合需求。

最终解决方案:直接 Print + Sync

fmt.Print(accumulated[printedLen:])
os.Stdout.Sync()

这样:

  1. 字符串自然累积增长
  2. 终端自动处理换行(满一行自动 wrap
  3. 保留所有历史输出
  4. 每次刷新缓冲区确保立即显示

这正是 ollama 等工具的流式输出效果。


12. 使用 bubbletea v2 的 spinner 组件实现加载动画

需求分析

用户希望在使用流式输出时,显示加载动画:

  • 用户输入后显示 "思考中... ⠋"
  • 第一个 token 返回后显示 "思考完成."
  • 流式输出完成后添加空行分隔

技术选型

使用 charm.land/bubbles/v2/spinner 组件,这是 bubbletea v2 官方提供的 spinner 组件。

实现方案

创建独立的 Spinner 结构体,在独立 goroutine 中运行动画:

type Spinner struct {
    text    string
    state   SpinnerState
    spinner spinner.Model
    stopCh  chan struct{}
    doneCh  chan struct{}
}

关键点:

  • 使用 spinner.MiniDot 动画样式
  • 独立 goroutine 使用 ticker 驱动动画帧切换
  • 使用 \r 回车符在同一行刷新动画
  • Stop 时输出 "思考完成."

官方示例参考

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case spinner.TickMsg:
        var cmd tea.Cmd
        m.spinner, cmd = m.spinner.Update(msg)
        return m, cmd
    }
}

关键spinner.Tick() 返回的 TickMsg 需要传给 spinner.Update(),并使用返回值更新 spinner model。

注意事项

  1. spinner model 更新必须使用返回值:

    s.spinner, _ = s.spinner.Update(msg)  // 正确
    s.spinner.Update(msg)  // 错误!动画不会动
    
  2. 动画位置:动画在前,文字在后:

    fmt.Printf("\r%s %s", s.spinner.View(), s.text)  // ⠋ 思考中...
    
  3. 换行控制:

    • "思考完成." 后需要两个换行符(一个换行 + 一个空行)
    • 流式输出完成后也需要空行分隔

13. 重绘残留问题与新流程

问题描述

之前的流程:

  1. 流式实时打印 token边收边打
  2. 完成后 Markdown 重绘
  3. 问题:重绘有残留

解决方案:等待完整响应后输出

改进后的流程:

  1. AI 返回完整数据 ← 等待时间
  2. Markdown 转译
  3. 模拟流式输出(从配置读取速度)

效果更好,无残留问题。

配置化

使用 project.config.yml 统一管理配置:

streaming:
  line_delay_ms: 1000     # 每行输出后的延迟(毫秒)
  last_line_delay_ms: 600  # 最后一行延迟(毫秒)

markdown:
  glamour_style: dark
  wrap_width: 0       # 自动获取终端宽度

ui:
  logo: "🦐"
  user_prefix: "👀 "  # 用户输入前缀

14. 按行延迟输出的实现

核心逻辑

func outputLineByLine(text string) {
    lines := strings.Split(text, "\n")
    totalLines := len(lines)

    cfg := internal.GetProjectConfig()
    lineDelay := time.Duration(cfg.Streaming.LineDelayMs) * time.Millisecond
    lastLineDelay := time.Duration(cfg.Streaming.LastLineDelayMs) * time.Millisecond

    for i, line := range lines {
        if line == "" {
            fmt.Println()
            continue
        }
        fmt.Println(line)

        if i < totalLines-1 {
            time.Sleep(lineDelay)
        } else {
            time.Sleep(lastLineDelay)
        }
    }
    fmt.Println()
}

特点:

  • 空行直接跳过
  • 每行输出后延迟可配置
  • 最后一行延迟可单独配置

15. 工具调用结果显示问题

问题

使用 ChatStream 时,工具调用结果不显示。

原因分析

  1. 工具调用结果不在流式响应中返回,而是通过 bus.PublishOutbound() 单独发送
  2. ChatStreamonChunk 回调只处理文本内容,不处理工具调用
  3. 工具调用在 runTurn 循环中执行,结果通过消息总线发送

解决方案

回退使用 ProcessDirect,因为它会正确处理:

  • 工具调用流程
  • 工具结果显示
  • Markdown 渲染和按行输出

16. 状态栏优化

改动内容

  • 图标:
  • 图标颜色:#f0c75e
  • 文字颜色:#2b2e32
  • 内容:只显示耗时,如 ▣ 耗时: 2.3s

之前 vs 之后

  • 之前:▣ Tokens: 120 · 耗时: 2.3s · 总Tokens: 350
  • 之后:▣ 耗时: 2.3s

17. 项目配置文件详解

project.config.yml 结构

# hxclaw 项目配置文件

# 模拟流式输出配置
streaming:
  line_delay_ms: 1000      # 每行输出后的延迟(毫秒)
  last_line_delay_ms: 600  # 最后一行延迟(毫秒)

# Markdown 渲染配置
markdown:
  glamour_style: dark      # 渲染主题dark, light, dracula, tokyo-night 等
  wrap_width: 0           # 自动换行宽度0=自动获取终端宽度)

# UI 配置
ui:
  logo: "🦐"              # Logo
  user_prefix: "👀 "      # 用户输入前缀

# TTS 语音配置
tts:
  enabled: false          # 全局开关(默认关闭)
  port: 9876              # daemon 端口
  auto: true              # AI 回复后自动朗读

配置加载优先级

  1. 环境变量 HXCLAW_CONFIG 指定路径
  2. 项目根目录 project.config.yml

代码实现

// internal/config.go
type ProjectConfig struct {
    Streaming StreamingConfig `yaml:"streaming"`
    Markdown  MarkdownConfig  `yaml:"markdown"`
    UI        UIConfig        `yaml:"ui"`
}

func getConfigPath() string {
    if path := os.Getenv("HXCLAW_CONFIG"); path != "" {
        return path
    }
    return filepath.Join(".", "project.config.yml")
}

18. 行业经验参考

CLI 动画最佳实践

  • 帧率75ms/帧(约 13fps- GitHub Copilot CLI
  • Spinner 动画70-120ms - ora 库
  • AI 流式输出30-80ms/字符或行
  • 总动画时长:控制在 3 秒内 - Copilot CLI 原则

关键结论

  • 人眼需要约 30-50ms 才能感知单次视觉变化
  • 空白字符不应逐个输出,应批量处理
  • 终端宽度 100% 时 Markdown 渲染会显著增加行数和字符数

19. TTS 语音朗读集成

架构设计

hxclaw 作为 mimo-tts 的客户端,通过 TCP Socket 连接本地 daemon

hxclaw (客户端)  --TCP:9876-->  mimo-tts daemon (服务端)
                                      |
                                      v
                                API 调用 (mimo-v2.5-tts)
                                      |
                                      v
                                返回音频文件路径
                                      |
                                      v
                                afplay 播放

配置文件

tts:
  enabled: false   # 全局开关(默认关闭)
  port: 9876       # daemon 端口
  auto: true       # AI 回复后自动朗读

命令支持

输入 行为
/tts 切换 TTS 开关
/tts on 开启 TTS
/tts off 关闭 TTS
/tts status 显示状态
T 消息 临时开启并发送

动态提示符

  • 关闭:👀
  • 开启:👀 🔊

实现要点

  1. TCP 连接:使用 Go 标准库 net
  2. JSON 请求:发送格式 {"text": "内容"}
  3. 异步朗读:使用 go func() 异步调用
  4. 静默失败:网络异常只记录警告日志,不阻塞用户

踩坑记录

ergochat/readline SetPrompt 无返回值

// 错误
func (r *Readline) SetPrompt(prompt string) error {
    return r.rl.SetPrompt(prompt)  // SetPrompt 返回 void
}

// 正确
func (r *Readline) SetPrompt(prompt string) {
    r.rl.SetPrompt(prompt)  // void 类型
}

20. 禁用 picoclaw session 历史,实现独立上下文系统

问题背景

  • picoclaw 的 session 历史会被自动清空
  • 不利于 hxclaw 的会话连续性
  • 需要实现自我控制的上下文系统

解决方案

  • 禁用 picoclaw 的 session 历史读取
  • 使用 hxclaw 自己的 libSQL 数据库存储会话摘要
  • 在 ProcessDirect() 调用前注入上下文摘要到用户输入

实现步骤

  1. 创建 GetContextPrompt() - cmd/hxclaw/internal/memory/save.go
    func GetContextPrompt() string {
        // 从 hxclaw 自己的数据库获取会话摘要
        return "=== 当前会话摘要 ===
    

" + session.Summary + "

" }


2. **注入上下文** - `cmd/hxclaw/main.go`
```go
if memoryCfg.Enabled {
    contextPrompt := memory.GetContextPrompt()
    if contextPrompt != "" {
        input = contextPrompt + "
用户新问题: " + input
    }
}
resp, err := agentLoop.ProcessDirect(context.Background(), input, sessionKey)

效果

  • hxclaw 完全独立于 picoclaw session 管理
  • 会话摘要通过数据库持久化
  • 上下文通过摘要注入传递

21. UI 合并显示与颜色设计

需求

将原来分两行显示的信息合并为一行:

  • 之前:[memory] 已保存,当前会话 8 条消息 + ▣ 耗时: 20.3s
  • 之后:▣ 耗时: 20.3s · 会话已保存 · 当前会话 8 条消息

颜色设计

文字 颜色 十六进制
▣ (图标) 金色 #f0c75e
灰色文字 灰色 #2b2e32
会话已保存 暗绿色 #4a9e6b
会话保存异常 暗红色 #c75050

代码实现

var (
    iconStyle      = lipgloss.NewStyle().Foreground(lipgloss.Color("#f0c75e"))
    textStyle      = lipgloss.NewStyle().Foreground(lipgloss.Color("#2b2e32"))
    memoryOkStyle  = lipgloss.NewStyle().Foreground(lipgloss.Color("#4a9e6b"))
    memoryErrStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#c75050"))
)

func printElapsed(elapsed time.Duration, chatCount int, saveErr error) {
    icon := iconStyle.Render("▣  ")
    timeText := textStyle.Render(fmt.Sprintf("耗时: %s", elapsedStr))

    var statusText string
    if saveErr != nil {
        statusText = memoryErrStyle.Render("会话保存异常")
    } else if chatCount > 0 {
        statusText = memoryOkStyle.Render("会话已保存")
    }

    memCountText := textStyle.Render(fmt.Sprintf("当前会话 %d 条消息", chatCount))
    fmt.Printf("  %s%s · %s · %s

", icon, timeText, statusText, memCountText)
}

关键点

  • SaveChat() 改为返回 (chatCount int, err error),便于错误处理
  • 状态文字单独使用颜色样式
  • 失败时显示"会话保存异常"(暗红色)