Files
HxClaw/taolun.md
titor 3f9443c14b
Some checks failed
Release / build (push) Failing after 43s
docs: 更新讨论记录,添加 v0.1.0 相关知识点
2026-04-15 06:14:54 +08:00

9.7 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 家族库
  • lipgloss终端样式
  • glow代码高亮
  • 流程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: "👀 "      # 用户输入前缀

配置加载优先级

  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 渲染会显著增加行数和字符数