- 创建 hxclaw 项目,基于 picoclaw 的 CLI 增强工具 - 实现流式输出,使用 fmt.Print + os.Stdout.Sync() 实时刷新 - 解决 onChunk 回调累积文本导致的重复输出问题 - 使用 strings.Builder 收集完整响应并保存到 session - 添加讨论记录和更新日志文档
6.3 KiB
hxclaw 更新日志
版本记录
v0.1.0 (规划中)
- 创建 hxclaw 项目
- 实现流式输出功能
- Markdown 渲染功能(待实现)
- 代码高亮功能(待实现)
待实现功能
v0.1.0 (当前)
- 流式输出功能
- 导入 picoclaw 核心库
- 实现流式 Provider 调用
- 实时打印 token
- 处理非流式 Provider 回退
v0.2.0 (计划)
- Markdown 渲染
- Markdown 解析
- 基础样式(粗体、斜体、链接)
- 代码块渲染
- 表格渲染
- 列表渲染
v0.3.0 (计划)
- 代码高亮
- 集成 glow 或类似库
- 支持常见语言语法高亮
目前进度
- 创建项目目录结构
- 编写讨论记录(taolun.md)
- 编写更新日志(changelog.md)
- 编写 AI 行为指南(agents.md)
- 创建 go.mod
- 实现 main.go 入口
- 实现流式输出核心逻辑
- 编译成功,生成 hxclaw 二进制
认知纠正(踩坑记录)
Go replace 机制不需要发布到 registry
问题:最初担心需要像 npm 那样发布到 registry 才能被其他项目引用
纠正:Go 的 replace 机制可以直接指向:
- 本地路径(如
../picoclaw) - GitHub 仓库 + tag(如
github.com/sipeed/picoclaw v0.2.4)
知识点:Go 模块不需要发布到任何 registry,GitHub 就是事实上的 registry
hxclaw 不需要实现全部 picoclaw 功能
问题:最初担心需要自己实现 onboard、tools、mcp 等全部功能
纠正:hxclaw 是 CLI 增强层,只替换交互逻辑。picoclaw 的核心功能(agent loop、tools、mcp、skills)通过导入其 pkg 即可复用
知识点:采用组合优于继承的设计,需要什么功能就导入对应的包
流式输出需要判断 Provider 是否支持
问题:不是所有 Provider 都支持流式输出
纠正:需要使用类型断言判断 Provider 是否实现 providers.StreamingProvider 接口:
if sp, ok := provider.(providers.StreamingProvider); ok {
// 使用 ChatStream
} else {
// 使用普通 Chat
}
知识点:picoclaw 的 Provider 设计使用了接口分离原则,流式是可选能力
终端渲染使用 charmbracelet 库
问题:如何实现 Markdown 终端渲染
纠正:使用 charmbracelet 家族:
- lipgloss:样式定义
- glow:代码高亮
知识点:charmbracelet 是 Go 终端UI 的事实标准,API 设计优雅
独立二进制部署方式
问题:hxclaw 和 picoclaw 的关系
纠正:hxclaw 作为独立二进制,用户可以同时保留两个命令:
picoclaw agent使用原版hxclaw使用增强版
知识点:通过 go.mod replace 实现依赖绑定,用户无需安装 picoclaw 源码
AgentRegistry 没有 BuildMessages 方法
问题:最初尝试调用 agentLoop.GetRegistry().BuildMessages() 构建消息
纠正:BuildMessages 属于 ContextBuilder,不是 AgentRegistry:
// 正确方式
agentInstance.ContextBuilder.BuildMessages(history, summary, input, media, channel, chatID, senderID, senderDisplayName)
知识点:picoclaw 代码结构中,ContextBuilder 负责消息构建,AgentRegistry 负责 agent 管理
ToolDefinitions 获取方式
问题:如何获取可用的工具定义列表
纠正:通过 ToolRegistry 的 ToProviderDefs 方法:
toolDefs := agentInstance.Tools.ToProviderDefs()
知识点:ToolRegistry 维护工具注册,ToProviderDefs 转换为 provider 可用的格式
流式输出实时刷新
问题:流式输出时字符不是实时显示,要等很久才一次性出现
纠正:在 onChunk 回调中添加 os.Stdout.Sync() 强制刷新 stdout:
func(token string) {
fmt.Print(token)
os.Stdout.Sync() // 强制刷新
}
知识点:Go 的 fmt.Print 使用缓冲输出,需要手动刷新才能实时显示
Session 历史消息获取
问题:如何获取会话历史用于流式调用
纠正:通过 SessionStore 接口:
history := agentInstance.Sessions.GetHistory(sessionKey)
summary := agentInstance.Sessions.GetSummary(sessionKey)
知识点:AgentInstance.Sessions 实现了 SessionStore 接口,支持 GetHistory 和 GetSummary 方法
流式调用后的消息保存
问题:流式调用绕过了 agent loop,消息没有保存到 session
纠正:流式调用后手动保存消息:
agentInstance.Sessions.AddMessage(sessionKey, "user", input)
agentInstance.Sessions.AddMessage(sessionKey, "assistant", result)
知识点:SessionStore 接口提供 AddMessage 方法,支持 "user" 和 "assistant" 角色
onChunk 回调接收累积文本导致重复输出
问题:picoclaw 的 StreamingProvider 接口定义:
onChunk func(accumulated string)
注释说明:"onChunk receives the accumulated text so far (not individual deltas)"。每次回调时参数是累积的完整文本(如 "你好" → "你好!再次" → "你好!再次见到"),而不是增量。
纠正:使用 printedLen 跟踪已打印位置,只打印新增部分:
var printedLen int
func(accumulated string) {
if len(accumulated) > printedLen {
fmt.Print(accumulated[printedLen:])
printedLen = len(accumulated)
}
}
知识点:picoclaw 故意设计为累积文本,这样可以在任意时刻获取完整内容用于调试
尝试 uilive 库但只显示最后一行
问题:为了实现同行流动效果,尝试使用 github.com/gosuri/uilive 库
现象:该库会覆盖每一行,只显示最后一行内容
纠正:移除 uilive,直接使用 fmt.Print + os.Stdout.Sync(),让终端自然处理换行
知识点:uilive 适用于进度条等场景,不适合长文本流式输出
流式输出期望同行流动但实际换行显示
问题:用户期望像 ollama 那样在同行逐字符流动
最终方案:
fmt.Print(accumulated[printedLen:])
os.Stdout.Sync()
效果:
- 字符串自然累积增长
- 终端自动处理换行(满一行自动 wrap)
- 保留所有历史输出
- 每次刷新缓冲区确保立即显示
知识点:最简单的方案就是最有效的方案,不需要额外库