pyxchat:基于 Python 与 OpenAI 兼容 API 的命令行多轮对话项目

ai 编写的 python react agent

分支1Tags1
文件最后提交记录最后更新时间
12 天前
12 天前
12 天前
12 天前
22 天前
17 天前
12 天前
22 天前
12 天前
22 天前
22 天前

pyxchat (xml_chat 项目原用名)

一个 Python 3.9+ 命令行脚本,用 XML 文件管理多轮对话,通过 OpenAI 兼容 API 调用 LLM。

特性

  • XML 即历史:<chat><system/><user/><assistant/>…</chat>,人类可读、可手改
  • 持久化:每次用户输入和模型回复都实时原子写入 XML 文件,中途崩溃不损坏
  • OpenAI 兼容:支持任意 OpenAI 兼容服务(OpenAI、Azure、DeepSeek、Moonshot、Ollama 兼容模式等)
  • 流式输出:默认开启,关闭用 --no-stream 或环境变量 NO_STREAM=1
  • 内置工具 + 沙箱:read_file/write_file/edit_file/list_dir/grep/glob/shell/get_time 开箱即用,文件工具始终限制在沙箱 root 内,shell 可用 bwrap 隔离
  • 斜杠命令:/clear /show /system /save /exit /help

uv 安装

uv add -r requirements.txt
uv pip install -e .

运行程序

uv run xchat -h

运行结果示例

usage: xchat [-h] [--xml XML] [--provider PROVIDER] [--config CONFIG] [--model MODEL] [--base-url BASE_URL] [--api-key API_KEY] [--system SYSTEM]
             [--no-stream] [--no-auto-reply] [--list-providers] [--init-config] [--wizard] [--no-wizard] [--yes] [--no-sandbox] [-w DIR] [-i]
             [--no-tokens] [-a] [--tools] [-t TOOLS] [-c FILE] [-m TEXT]

用 XML 管理多轮对话的 OpenAI 兼容 LLM 客户端

options:
  -h, --help            show this help message and exit
  --xml XML, -x XML     会话 XML 文件路径(默认: chat.xml)
  --provider PROVIDER, -p PROVIDER
                        使用 ini 中 [name] 段配置(可由环境变量 XML_CHAT_PROVIDER 覆盖)
  --config CONFIG       ini 配置文件路径(默认查找 ./xml_chat.ini 或 ~/.xml_chat.ini)
  --model MODEL         模型名(可由 provider.models[0] / OPENAI_MODEL 覆盖)
  --base-url BASE_URL   API base URL(覆盖 provider 配置)
  --api-key API_KEY     API key(覆盖 provider 配置)
  --system SYSTEM       当 XML 不存在或没有 system 节点时使用的初始 system prompt
  --no-stream           关闭流式输出
  --no-auto-reply       关闭自动触发:即使 XML 末尾是 <user> 也不自动调 LLM
  --list-providers      列出 ini 中所有 provider 配置并退出
  --init-config         把示例 ini 模板打到 stdout(可用 > xml_chat.ini 重定向)并退出
  --wizard              强制启动配置向导(即使 XML 已存在也重新生成)
  --no-wizard           XML 不存在时跳过自动向导,直接创建空 XML
  --yes, -y             工具执行前不再 y/N 确认(危险!)
  --no-sandbox          禁用沙箱(即使 ini 的 [sandbox] enabled=true)
  -w DIR, --workdir DIR
                        把 DIR 写入 XML <chat> 的 sandbox 属性,作为沙箱 root / 工作目录(优先于 ini [sandbox] 的 root,默认当前目录)
  -i, --interactive     进入交互式 >>> REPL(默认只处理 XML 末尾的 <user> 后退出)
  --no-tokens           关闭每轮 token 统计显示(默认开启)
  -a, --agent           把全部内置工具以 <tool name="..."/> 声明写入 XML 后退出(不进入对话)
  --tools               以逗号分隔列出全部内置工具名并退出
  -t TOOLS, --tool TOOLS
                        把指定工具加入会话(逗号/空格分隔,如 -t shell,read_file),会以 <tool name="..."/> 声明写入 XML(已声明的跳过)
  -c FILE, --clone FILE
                        从指定 XML 提取 system、工具声明、sandbox 属性和 meta,创建新的 --xml 文件后退出(不进入对话)
  -m TEXT, --continue-message TEXT
                        执行一次对话后退出(非交互模式),不进入 >>> REPL

pip 安装

pip install -r requirements.txt

依赖:

  • openai>=1.0.0 — 调用 LLM
  • lxml>=5.0 — XML 读写(用 CDATA 包裹内容,人眼更可读;自动处理 ]]> 边界)
  • prompt_toolkit>=3.0 — 多行编辑(Enter 提交、Ctrl+J 换行、Esc+Enter 备用提交、跨平台、历史持久化、异步友好)
    • 未装时降级到 readline(Unix 自带)/ pyreadline3(Windows)单行模式

快速开始

# 1) 生成 ini 配置模板
python chat.py --init-config > xml_chat.ini
# 编辑 xml_chat.ini,填入你的 API key 或者用 ${ENV_VAR} 引用环境变量

# 2) 启动交互模式(REPL,会一直等输入)
python chat.py --provider openai --system "你是一个简洁的中文助理"

# 3) 一次性模式:执行一次对话后退出(适合脚本/管道)
python chat.py -c "Python 是什么?" --provider openai

# 4) 切换到 deepseek
python chat.py --provider deepseek --xml deepseek-chat.xml

# 5) 列出当前所有 provider
python chat.py --list-providers

如果你不想用 ini,仍然可以直接用环境变量:

export OPENAI_API_KEY="sk-..."
export OPENAI_BASE_URL="https://api.openai.com/v1"   # 可选
export OPENAI_MODEL="gpt-4o-mini"                     # 可选
python chat.py --system "你是一个简洁的中文助理"

配置文件

ini 文件按以下顺序自动查找,找到第一个就用:

  1. --config / -c 显式指定的路径
  2. ./xml_chat.ini
  3. ~/.xml_chat.ini

也可以用环境变量 XML_CHAT_PROVIDER 指定默认 provider(等价于每次都加 --provider)。

格式

; 分号开头是注释
[default]                              ; 不传 --provider 时默认用这个
base_url      = https://api.openai.com/v1
api_key       = ${OPENAI_API_KEY}      ; 写死也行,比如 sk-xxxx
models        = gpt-4o-mini, gpt-4o, gpt-3.5-turbo
default_model = gpt-4o-mini            ; 不传 --model 时用这个;留空则取 models 第一个

[deepseek]
base_url      = https://api.deepseek.com/v1
api_key       =
models        = deepseek-chat, deepseek-reasoner
default_model = deepseek-chat

[ollama]
base_url      = http://localhost:11434/v1
api_key       = ollama
models        = llama3, qwen2
default_model = llama3

支持的 key:

  • base_url:OpenAI 兼容的 API base URL
  • api_key:API key;值里可以用 ${ENV_VAR} 引用环境变量,缺失时保持原样
  • models:逗号分隔的可用模型列表(--list-providers 展示用)
  • default_model:不传 --model 时用的默认模型;留空则回退到 models 第一个
  • default_model 允许不在 models 列表里(支持未列出的实验性模型)

优先级(从高到低)

provider 选择(没传 --provider 时):

情况 行为
--provider <name> / XML_CHAT_PROVIDER 用 ini 中对应段
ini 中有名为 default 的段 用 default 段
ini 中只有 1 个 provider 自动用它(免去 --provider 参数)
ini 中多个 provider 都没标 default 报错,提示加 [default] 段或显式 --provider
ini 不存在 走环境变量,不选 provider

同字段取值(从选中的 provider 出发):

来源 行为
命令行 --api-key / --base-url / --model 完全覆盖 provider 的对应字段
provider 的 base_url / api_key / models[0] provider 提供默认值
环境变量 OPENAI_API_KEY / OPENAI_BASE_URL / OPENAI_MODEL provider 没值时兜底

典型场景:

  • python chat.py → 啥都没传,自动用 ~/.xml_chat.ini 里的 default 段
  • python chat.py --model gpt-4o → 还是用 default,但 model 换成 gpt-4o
  • python chat.py --provider deepseek → 切到 deepseek,model 用其 models[0]
  • python chat.py --provider deepseek --model deepseek-reasoner → 显式选 model

命令行参数

参数 默认 说明
--xml chat.xml 会话 XML 文件路径
--provider / -p XML_CHAT_PROVIDER 或 ini 的 default 选 ini 中的 provider 段
--config ./xml_chat.ini 或 ~/.xml_chat.ini ini 配置文件路径
--model provider.default_model / models[0] / OPENAI_MODEL 模型名
--base-url OPENAI_BASE_URL API base URL(覆盖 provider)
--api-key OPENAI_API_KEY API key(覆盖 provider)
--system 无 XML 首次创建且无 system 节点时,注入 system prompt
--no-stream 关闭流式 关闭流式输出
--no-auto-reply 自动触发开启 关闭自动触发:即使 XML 末位是 <user> 也不自动调 LLM
--list-providers — 列出 ini 中所有 provider / tool 后退出
--init-config — 输出 ini 模板到 stdout 后退出
--wizard — 强制启动配置向导(重生成 XML)
--no-wizard 自动向导开启 XML 不存在时跳过自动向导
--yes / -y 关闭 工具执行前不再 y/N 确认(危险!)
--no-sandbox 沙箱按 ini 配置 禁用沙箱(即使 ini 的 [sandbox] enabled=true),等价 XML_CHAT_NO_SANDBOX=1
-c / --continue-content 交互 执行一次对话后退出(非交互模式,适合脚本)

运行时命令

命令 说明
/exit 退出
/clear 清空所有历史(需二次确认,保留 tool 声明,同时清零 token 计数)
/show pretty-print 当前 XML
/system <text> 设置或替换 system prompt
/save [path] 把当前 XML 以 pretty 形式另存
/tokens 查看累计 token 用量(in / out / cache / turns)
/help 显示帮助

Spinner + Token 统计

Spinner:等待 LLM 响应时,后台线程用 \|/-\ 旋转提示,响应到达后自动擦除。流式模式下在第一个 chunk 到达时停,避免和流式输出打架。

Token 统计:每次 API 响应带的 usage 字段被自动累加,本轮结束后打印一行:

[tokens] 本轮 1 步  in=100  out=30  cache=50  total turns=1

字段含义:

  • in — 本次请求的 prompt token 数
  • out — 本次请求的 completion token 数
  • cache — prompt 中被 prompt cache 命中的 token(OpenAI / DeepSeek 等支持)
  • turns — 累计 API 调用次数(ReAct 多步工具调用算多次)

大数字自动用 K / M / B 缩位:

范围 格式 例子
< 1,000 原样 in=856
1,000–9,999 X.XK in=1.2K
10,000–999,999 XK in=50K
1,000,000–9,999,999 X.XM in=1.6M
≥ 10,000,000 XM in=85M
≥ 1,000,000,000 X.XB in=2.0B

/tokens 看累计;--no-tokens 关闭显示;/clear 会同时清零计数。

工具调用 (ReAct 模式)

轻量版工具系统:内置工具开箱即用,ini 里可定义自定义工具(同名覆盖内置),XML 里只声明用哪些,实际调用时只把子集传给 LLM。

0. 内置工具

脚本自带 8 个常用工具(Python 实现,不走 shell 模板),无需在 ini 中定义,XML 里声明即可用:

工具 参数 risk 说明
read_file path read 读文件,限沙箱 root 内,10KB 截断
write_file path, content write 写/覆盖文件,限 root 内,自动建父目录
edit_file path, old, new write 替换文件中唯一出现的一段文本(多次出现会报错,防误改)
list_dir path(可选) read 列目录,目录带 / 后缀
grep pattern, path(可选) read 递归正则搜索,跳过 .git/.venv 等,上限 100 条
glob pattern read 按 glob 找文件,上限 200 条
shell command shell 执行 shell 命令(沙箱 enabled 时走 bwrap)
get_time 无 read 当前时间

在 ini 中定义同名的 [tools.read_file] 等可以覆盖对应内置工具。

沙箱

ini 中加 [sandbox] 段:

[sandbox]
enabled = false      ; true 时 shell 命令用 bwrap 隔离
backend = auto       ; auto(有 bwrap 就用)| bwrap(强制,不可用则拒绝执行)| none
root    = .          ; 文件工具 / shell 的工作目录,默认当前目录
network = false      ; bwrap 下是否保留网络

两层防护:

  1. 文件工具路径 jail(始终生效,与 enabled 无关):read_file/write_file/edit_file/list_dir/grep/glob 的路径被解析到 root 内,../、绝对路径越界直接报 [沙箱拦截]。
  2. shell 的 bwrap 隔离(enabled=true 时):系统目录只读挂载、仅 root 可写、/tmp 用 tmpfs、默认 --unshare-net 断网、cwd 设为 root。

backend=auto 时会先用一条最小命令实测 bwrap 能否运行(有些内核禁用了 unprivileged user namespace,bwrap 装了也跑不起来);实测失败则警告后降级为本机直接执行。--no-sandbox 或 XML_CHAT_NO_SANDBOX=1 整体关闭(bwrap 部分;文件 jail 仍生效)。

1. ini 中定义工具

[tools.get_weather]
description = 查询某城市天气
command     = curl -s "https://wttr.in/{city}?format=3"
params      = city
risk        = shell              ; 可选,默认 shell

[tools.read_file]
description = 读取文件内容,参数 path
command     = cat {path}
params      = path
risk        = read               ; 读类免确认

command 是 shell 模板,{param} 会被替换为 LLM 传入的参数(自动 shlex.quote 防注入)。30 秒超时,10KB 输出截断。

risk 字段(可选,默认 shell):

值 行为
read 免确认(只读操作,比如 cat、head、ls)
write 确认一次(写文件、改状态)
shell 确认一次,且命中危险黑名单时多一次高亮确认

2. XML 中声明使用哪些

<chat>
  <system>你可以使用工具。</system>
  <tool name="get_weather"/>
  <tool name="get_time"/>
  <user>北京现在几点?天气如何?</user>
</chat>

<tool name="..."/> 是声明节点(不会被发给 LLM),只是告诉脚本"我这场对话要用这些工具"。真实调 API 时,只把声明的子集作为 tools=[...] 传过去。

3. ReAct 循环

启动后,LLM 在每轮可以调多个工具 → 我们执行 → 把结果用 role="tool" 回传 → LLM 再决策 → 直到它给不含 tool_calls 的纯文本,就是 Final Answer。最大 10 步防死循环。

XML 完整记录 ReAct 过程:

<user><![CDATA[
北京现在几点?天气如何?
]]></user>
<assistant tool_calls="[{...}]"/>           <!-- 决策:调 get_weather -->
<tool tool_call_id="call_xxx"><![CDATA[
北京 25°C 晴
]]></tool>
<assistant tool_calls="[{...}]"/>           <!-- 再决策:调 get_time -->
<tool tool_call_id="call_yyy"><![CDATA[
2026-08-30 10:00
]]></tool>
<assistant><![CDATA[
北京25度晴,现在10点。
]]></assistant>  <!-- Final Answer -->

4. 安全

确认机制分三层:

  1. risk=read — 直接执行,免确认
  2. risk=write / shell — 执行前 y/N 确认一次
  3. 危险模式命中 — 高亮红框展示完整命令 + 触发原因,多一次 仍要执行? 确认(默认 N)

内置危险黑名单(正则匹配最终渲染的命令字符串):

  • :(){ :|:& };: 及其变体(fork bomb)
  • rm -rf / rm -fr 类(任意路径)
  • mkfs、dd of=/dev/sdX、>/dev/sdX(磁盘写操作)
  • chmod 777 / chmod -R 777
  • curl | sh / wget | sh(远程管道)
  • shutdown / reboot / poweroff / halt
  • eval、sudo

示例危险拦截输出:

============================================================
[危险!] 工具 'cleanup' 触发拦截
  原因: rm -rf
  完整命令: rm -rf /var/log/*
  参数: {'path': '/var/log/*'}
============================================================
  仍要执行? [y/N]:

批量自动化用 --yes / -y / XML_CHAT_YES=1 跳过所有确认(包括危险的二次确认),慎用!

5. 配置向导

XML 不存在时,如果有 ini 且里面定义了工具,会自动进入向导引导你输入 system、选择工具、写首个 user(直接回车可留空)。不想用向导加 --no-wizard;想强制重做加 --wizard。

6. 交互模式行编辑(prompt_toolkit)

启动时自动启用 prompt_toolkit(多行编辑),如果没装就降级到 readline。

按键 行为
Enter 提交当前输入(multiline 模式默认是换行,这里覆盖)
Ctrl+J 换行(在 user 内容里插入真换行)
Esc+Enter 备用提交
Ctrl+D 提交(空行时 EOF 退出)
Ctrl+C 立即退出(打印 Bye~)
↑ / ↓ 历史(持久化到 ~/.xml_chat_history)
← → 移动光标
Ctrl+A / Ctrl+E 跳到行首/行尾
Backspace 删除光标前字符

多行内容会原样保留到 XML:按 Ctrl+J 换行,内容里的 \n 会写入 <user> 节点(用 CDATA 包裹),LLM 看到的就是完整的多段文本。

7. 一次性模式(-c / --continue-content)

-c "<内容>" 让脚本执行一次对话后立即退出,不进入 >>> REPL,适合 shell 脚本、管道、定时任务。

# 一次提问,得到回答后退出(exit 0)
python chat.py -c "Python 列表推导式是什么?" 

# 接续已有 XML 继续聊
python chat.py -c "再举几个例子"

# 串到其他工具
python chat.py -c "总结这段代码" | tee log.txt

# 与 auto-reply 配合:XML 末位是 user,会先跑完 auto,再跑 -c
python chat.py -c "下一轮"

行为细节:

  • 不进入 REPL,不需要 stdin
  • 不触发 wizard,XML 不存在则报错退出(exit 1)
  • auto-reply 仍然执行:如果 XML 末位是 <user>,先自动完成那一轮,再执行 -c 的内容
  • 退出码:成功 0,API 错误 1
  • 与 --wizard 互斥,传了会忽略 --wizard

XML 格式说明

根节点 <chat>,子节点必须使用 system / user / assistant 三种 role 之一。

pretty 格式 + CDATA 包裹:脚本始终以 pretty 形式(每节点一行,2 空格缩进)写盘,所有 text 用 <![CDATA[ ... ]]> 包裹——CDATA 内部用换行符把内容夹在中间,而不是挤在一行。目的是方便 cat chat.xml 直接查看,内容一眼可读。

<?xml version="1.0" encoding="UTF-8"?>
<chat>
  <system><![CDATA[
你的系统提示词
]]></system>
  <user><![CDATA[
用户消息
]]></user>
  <assistant><![CDATA[
助手回复
]]></assistant>
  <user><![CDATA[
下一轮用户消息
]]></user>
  <assistant><![CDATA[
下一轮助手回复
]]></assistant>
</chat>

关于多轮续聊

启动时脚本会按出现顺序读取所有子节点,直接转成 OpenAI messages 传给 API。 你可以手工编辑 XML 来增删/修改历史,保存后下次启动脚本就会用新历史继续。 手工编辑时不需要手动加 CDATA 包裹,脚本下次保存时会自动重包(原 CDATA 在 parse 后会丢标记,但内容原样保留)。

关于 块(推理模型)

对于会在 content 里返回 <think>...</think> 块的模型(DeepSeek-R1、某些 o1 风格的开源模型等),脚本会:

  1. 抽取 think 块到独立的 <think> XML 节点,放在对应 <assistant> 之前(这样阅读 XML 时,推理过程在前、答案在后,更自然)
  2. 清洗后的 assistant 内容(只剩"实际答案")作为 <assistant> 节点的 text
  3. 构造新请求时跳过所有 <think> 节点,不污染上下文

这样 XML 既能完整回看每一步的推理过程,又不会让历史 think 反复回传给 LLM 占 token / 影响判断。

注意:不允许连续多个 <think> 元素——一个 <think> 对应其后一条 <assistant>, 手工编辑时请把多段思考合并进同一个节点,否则加载会报错。

如果确实需要把历史思考上下文回传给模型(部分 API 靠它延续推理),在 ini 的 provider 段设置 include_think:

  • include_think = true:思考内容以 <think>...</think> 文本拼进其后第一条 assistant 消息的 content(适合 llama.cpp 等本地服务)
  • include_think = reasoning:思考内容以原生 reasoning_content 字段挂在 assistant 消息上回发 {"role": "assistant", "content": ..., "reasoning_content": ...}(适合 GLM 等深度思考 API)

对于 GLM 等深度思考 API(thinking: {"type": "enabled"}),思考内容通过独立的 reasoning_content 字段返回(不在 content 里),脚本同样会解析并合并存入 <think> 节点; 流式模式下思考内容以暗色实时打印,正文开始后切换为青色。

关于 节点(会话级 LLM 参数)

<chat> 的第一个子元素可以是 <meta>,存放要传给 LLM 的采样/思考参数(不进 messages):

<chat>
  <meta>
    <thinking type="enabled"/>              <!-- enabled | disabled(GLM 深度思考) -->
    <reasoning_effort value="high"/>        <!-- max | xhigh | high | medium | low | minimal | none -->
    <temperature value="0.7"/>              <!-- 0.0 ~ 2.0 -->
  </meta>
  <system>...</system>
  ...
</chat>
  • temperature 作为标准字段传递;thinking / reasoning_effort 通过 extra_body 传递 (GLM https://open.bigmodel.cn/api/paas/v4/chat/completions 等兼容接口可识别)。
  • 非法值会打印警告并忽略;没有 <meta> 或子元素缺失时不传对应参数。
  • 新建会话(--wizard / 自动创建)会自带 <meta><thinking type="disabled"/></meta>; /clear 和 --from-xml 等操作会保留 <meta> 且保持其为第一个子元素。

<think> 节点的例子:

<user><![CDATA[
北京今天天气怎么样?
]]></user>
<think><![CDATA[
用户问北京天气,我应该用 get_weather 工具查
考虑 wttr.in 这个 API
构造调用...
]]></think>
<assistant><![CDATA[
北京25度,晴。
]]></assistant>
<user><![CDATA[
上海呢?
]]></user>
<think><![CDATA[
...
]]></think>
<assistant><![CDATA[
上海18度,阴。
]]></assistant>

关于自动触发(用 XML 初始化一段对话)

如果你想用 XML 预置一段对话让模型先答一句,只要把 XML 末位写成 <user>,启动脚本时会自动调用一次 LLM 并把 assistant 回复写回 XML。

例如新建 init.xml:

<?xml version="1.0" encoding="UTF-8"?>
<chat>
  <system>你是简洁的中文助理</system>
  <user>用一句话介绍自己</user>
</chat>

然后运行:

python chat.py --xml init.xml
# 启动时自动看到 LLM 的回复,无需手动输入
# 然后进入 >>> 提示符继续对话

如果 XML 末位是 <assistant>(也就是历史已经"完整"),脚本会等你手动输入,不会自动触发。需要强制关闭这个行为,加 --no-auto-reply。

关于特殊字符

由于 text 都用 CDATA 包裹,& < > " ' 等 XML 特殊字符在消息内容里不需要任何转义,直接写就行。脚本自动处理 ]]> 边界(若消息内容里出现这个序列,会被 lxml 自动拆分为两个 CDATA 段)。

例外:<assistant tool_calls="..."/> 和 <tool tool_call_id="..."/> 这两个节点的 tool_calls / tool_call_id 是 XML attribute,里面的 JSON 字符串会按 XML 规则做 attribute 转义(&lt; &gt; &quot; 等),但读回时脚本会自动反解,用户无需关心。

故障排查

  • 未找到 openai SDK → pip install -r requirements.txt
  • 缺少 API key → 设置 OPENAI_API_KEY 或用 --api-key
  • 无法解析 XML → 确认根节点是 <chat>,子节点 role 在 system/user/assistant 之中
  • API 错误导致 user 消息残留 → 脚本会自动回滚该条,下次重启不会带着失败的请求

Python 兼容性

代码仅使用 3.9 及之前可用的语法,from __future__ import annotations 进一步放宽注解写法。 依赖 openai>=1.0(其最低支持 Python 3.7+)。

项目介绍

ai 编写的 python react agent

定制我的领域