跳转到内容

第 29 节 · UI 与交互模式:REPL / Solo / 流式输出

一句话回答

Agent 写得再好,没有趁手的"开口子"也没人用。 REPL 让人能交互,Solo 让机器能调用,流式输出和工具可视化让用户知道它没卡死。

三种交互模式

模式一句话典型代表
REPL多轮交互,跟 Agent "聊一会儿"Cursor 侧边栏、Claude Code 终端、CodeBuddy 对话
Solo(一次性任务)给一个任务、跑完退出CI 脚本、claude --print、Cron / NPC 入口
流式输出边生成边展示,不让用户干等ChatGPT、Cursor 主面板、终端 agent

这不是三选一,是三件可以同时存在的事。一个完整的 Coding Agent 通常 REPL + Solo 都要支持,并且都应该让工具过程可见。

模式 ①:REPL(Read-Eval-Print Loop)

形态

$ python main.py
🤖 CodingAgent | workdir=. | tools=5

 帮我看看 main.py 里有什么
[工具调用 read("main.py")]
main.py 里是一个简单的 Flask 应用,定义了 ...

 给它加上 /health 路由
[工具调用 edit(...)]
 已添加 /health 路由

 /quit
👋 再见

核心循环

REPL 的最小循环

python
def run_repl(workdir):
    agent = CodingAgent(workdir=workdir)
    while True:
        try:
            user_input = input("").strip()
        except (EOFError, KeyboardInterrupt):
            break
        if not user_input:
            continue
        if user_input == "/quit":
            break
        if user_input == "/reset":
            agent.reset()
            continue

        answer = agent.chat(user_input)
        print(answer)

真正的 REPL 关注三件小事

细节为什么必要
捕获 Ctrl+C用户中断当前输入或当前任务时,不应该把整个进程杀掉
/reset 命令长对话跑久了 messages 太长,给用户一个清空按钮
工具调用要打印出来用户需要看见 Agent 正在做什么,不然像黑盒

参考实现:labs/06-coding-agent-integration/my_coding_agent/ui/repl.py。完整版还可以参考 examples/full-agent/main.py_run_repl

模式 ②:Solo(一次性任务)

形态

bash
# 跑一次就退出
python main.py --solo exec "总结 README"

# 结果输出到文件
python main.py --solo exec "在 src/ 下找出所有 TODO" -o todos.txt

# 走管道(CI/CD 友好)
python main.py --solo exec "列出依赖" -o - | tee deps.txt

为什么需要 Solo 模式

REPL 是"交互体验",Solo 是"自动化能力"。当你需要把 Agent 接进 CI / Cron / NPC / 调度脚本时,Solo 模式才是主战场:

  • ⏰ 每天凌晨自动审 PR
  • 🤖 CI 失败时让 Agent 看错误日志、写一份分析报告
  • 📦 上线后让 Agent 给本次发布生成 changelog
  • 🧑‍💼 仓库 NPC 被 @ 后自动跑一次任务并回写评论

REPL 在这些场景里不能用——没人坐在终端前回复 Agent 的提问。

实现要点

python
def run_solo(workdir, query, output_file=None):
    agent = CodingAgent(workdir=workdir)
    answer = agent.chat(query)

    if output_file == "-":
        sys.stdout.write(answer)              # 纯文本,方便管道
    elif output_file:
        with open(output_file, "w") as f:
            f.write(answer)
    else:
        print(answer)

关键设计:Solo 模式下,思考过程(工具调用、调试日志)走 stderr,最终结果走 stdout。这样用 >| tee 或 CI 捕获输出时,不会被中间过程污染。

python
# 看到这种写法不要慌:把 ui.console 切到 stderr
if output_file == "-":
    ui.console = Console(stderr=True)

参考:examples/full-agent/main.py_run_solo_exec

模式 ③:流式输出

为什么必须做

LLM 生成 1000 token 可能要几秒到十几秒。如果你等所有内容都生成完才一次性打印,用户会觉得:

"啊?是不是卡死了?"

而流式输出(一个字一个字吐)让用户能第一时间看到反应。即使总耗时一样,感受完全不同。

流式输出 vs 一次性输出 的等待感受

流式输出的 OpenAI 风格协议

python
# 非流式:一次性返回
resp = client.chat.completions.create(model="...", messages=...)
print(resp.choices[0].message.content)

# 流式:返回一个迭代器,每次 yield 一个 chunk
stream = client.chat.completions.create(model="...", messages=..., stream=True)
for chunk in stream:
    delta = chunk.choices[0].delta
    if delta.content:
        print(delta.content, end="", flush=True)

在 Chat Completions 风格接口里,stream=True 是最小开关。但真实客户端还要处理断线、取消、重试和工具调用分片。

流式 + 工具调用:一个坑

工具调用的流式数据是分片到达的——tool_call.function.arguments 可能在多个 chunk 里被切碎,必须自己拼回来:

python
tool_calls_buf = {}  # index -> 累积的 tool_call

for chunk in stream:
    delta = chunk.choices[0].delta
    if delta.content:
        print(delta.content, end="", flush=True)

    for tc_delta in (delta.tool_calls or []):
        idx = tc_delta.index
        if idx not in tool_calls_buf:
            tool_calls_buf[idx] = {"id": "", "name": "", "args": ""}
        if tc_delta.id:
            tool_calls_buf[idx]["id"] = tc_delta.id
        if tc_delta.function.name:
            tool_calls_buf[idx]["name"] = tc_delta.function.name
        if tc_delta.function.arguments:
            tool_calls_buf[idx]["args"] += tc_delta.function.arguments

这段代码看着繁琐,但 OpenAI 风格的工具流式调用经常就是这个模式。先跑通一次,以后忘了回来抄。

完整版的流式拼装逻辑:examples/full-agent/llm/llm.py_collect_tool_stream / _iter_stream

工具调用的"过程可视化"

工具调用的过程可视化

光流式吐文字还不够。Agent 调工具时,用户必须看到 Agent 在干嘛——不然就跟"卡死"一样:

 帮我看 main.py 里有什么

💭 思考: 先读一下文件内容
 read("main.py")
 from flask import Flask
     app = Flask(__name__)
     @app.route("/")
     def index(): ...
     ...

main.py 里是一个简单的 Flask 应用,包含 ...

3 个最简单的可视化技巧:

技巧怎么做
工具调用前打印参数print(f"⚡ {name}({args})")
工具结果打前 200 字print(f" → {result[:200]}")
区分 thinking / 工具 / 回答用 emoji(💭 / ⚡ / 🎯)或 Rich 颜色

进阶:用 rich 渲染面板,效果接近 IDE 里的工具调用面板。examples/full-agent/ui/ui.py 里有现成的 print_tool_call

2026 年 CLI Agent 的 UX 底线

2026 年很多 Coding Agent 又回到了 CLI,不是因为 UI 不重要,而是因为 CLI 最容易接进脚本、CI 和仓库机器人。一个可用的 CLI Agent 至少要做到:

能力为什么重要
可中断用户发现方向错了,能 Ctrl+C 回到输入
可复现Solo 命令能复制给队友或放进 CI
stdout / stderr 分离最终结果可被机器消费,过程日志不污染输出
工具透明知道它读了什么、改了什么、跑了什么
退出码清晰CI 需要知道成功还是失败

这些不是花哨体验,是 Agent 从 demo 走向工具的分界线。

把三种模式接进同一个 main.py

python
def main():
    args = parse_args()
    if args.solo:
        run_solo(args.workdir, args.query, args.output)
    else:
        run_repl(args.workdir)

只多几行就把 Solo 和 REPL 同时支持了。流式输出和工具可视化跟"REPL/Solo"是正交的:两种入口都可以用。

一个真实的体验对比

跑同样一个任务:"读 main.py,加 /health 路由",给三种 Agent 用户的感受:

配置用户感受
REPL + 非流式 + 工具调用不可见"怎么半天没反应?卡了?"(实际在跑工具)
REPL + 流式 + 工具调用不可见"在打字了!但它是不是在乱回?怎么没改文件?"
REPL + 流式 + 工具调用可见"哦它在 read 文件了 → 在 edit 了 → 改完了。完美。"

第三种才是现代 Coding Agent 的基本体验。今天 lab 至少要做到 REPL + 工具可视化;Solo 和更完整的 UI 在 examples/full-agent 里继续看。

小结

模式一句话
REPL长对话 / 交互体验 / 必须捕获 Ctrl+C
Solo一次性任务 / CI 和 NPC 友好 / stderr 装日志、stdout 装结果
流式输出stream=True + 拼 chunk / 让用户第一时间看到反应
工具可视化打印工具名 + 参数 + 截短结果 / 三个 emoji 就够

理论结束。下面 lab:

  • Part 1(80min):把 6 个零件整合成 my_coding_agent/,写完 agent.py + repl.py + main.py
  • Part 2(70min):用你的 Agent 完成 3 个真实任务(读自己 / 补单测 / 找 bug)
  • Part 3(15min):故障排查 + "我的 Agent 跟 Cursor 比差距在哪"

Released under the MIT License.