ChatGPT CLI 开发实战:从零构建高效命令行交互工具

1次阅读
没有评论

共计 3882 个字符,预计需要花费 10 分钟才能阅读完成。

image.webp

背景痛点

作为开发者,我经常需要在编写代码时快速查询技术问题或调试代码片段。网页版 ChatGPT 虽然强大,但在开发工作流中存在几个明显痛点:

ChatGPT CLI 开发实战:从零构建高效命令行交互工具

  1. 上下文丢失问题 :每次打开新会话都会丢失之前的对话历史,调试复杂问题时需要反复复制粘贴上下文
  2. 工作流中断 :需要在浏览器和 IDE/ 终端之间频繁切换,影响思维连贯性
  3. 功能局限 :无法直接执行代码片段或集成到现有开发工具链中
  4. 响应延迟 :网页界面加载和网络请求带来的额外延迟

技术选型

实现 CLI 工具主要有两种技术路线:

  1. LangChain 方案
  2. 优点:提供高层抽象,内置对话记忆等组件
  3. 缺点:依赖较重,定制灵活性较低

  4. 直接调用 OpenAI API

  5. 优点:轻量级,完全控制请求 / 响应流程
  6. 缺点:需要自行实现上下文管理等基础功能

考虑到我们需要高度定制化和性能优化,选择直接调用 API 方案。以下是核心模块的技术栈:

  • CLI 框架:Python 标准库 argparse
  • 网络请求:aiohttp(异步 HTTP 客户端)
  • 终端 UI:rich(富文本渲染)
  • 配置管理:configparser

核心实现

1. CLI 框架搭建

使用 argparse 构建基础命令行界面:

import argparse

def create_parser():
    parser = argparse.ArgumentParser(description='ChatGPT CLI Tool')
    parser.add_argument('query', nargs='?', help='直接查询内容')
    parser.add_argument('--session', help='指定会话名称')
    parser.add_argument('--model', default='gpt-3.5-turbo', 
                       help='指定模型版本')
    return parser

2. 异步 API 请求

使用 aiohttp 实现高性能异步请求:

import aiohttp

async def chat_completion(messages, api_key, model='gpt-3.5-turbo'):
    headers = {'Authorization': f'Bearer {api_key}',
        'Content-Type': 'application/json'
    }

    payload = {
        'model': model,
        'messages': messages,
        'temperature': 0.7
    }

    async with aiohttp.ClientSession() as session:
        async with session.post(
            'https://api.openai.com/v1/chat/completions',
            json=payload,
            headers=headers
        ) as response:
            return await response.json()

3. 上下文管理

实现带持久化的对话历史记录:

import json
import os

class ChatContext:
    def __init__(self, session_name='default'):
        self.session_name = session_name
        self.history = []
        self.load_history()

    def add_message(self, role, content):
        self.history.append({'role': role, 'content': content})
        self.persist_history()

    def persist_history(self):
        os.makedirs('sessions', exist_ok=True)
        with open(f'sessions/{self.session_name}.json', 'w') as f:
            json.dump(self.history, f)

    def load_history(self):
        try:
            with open(f'sessions/{self.session_name}.json') as f:
                self.history = json.load(f)
        except FileNotFoundError:
            self.history = []

4. 终端 Markdown 渲染

利用 rich 库实现美观的输出渲染:

from rich.console import Console
from rich.markdown import Markdown

console = Console()

def print_markdown(content):
    md = Markdown(content)
    console.print(md)

生产考量

1. 超时重试机制

import asyncio
from tenacity import retry, stop_after_attempt, wait_exponential

@retry(stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=1, min=4, max=10)
)
async def robust_request(messages, api_key, model):
    try:
        return await chat_completion(messages, api_key, model)
    except asyncio.TimeoutError:
        print('请求超时,正在重试...')
        raise

2. API 调用频次控制

import time

class RateLimiter:
    def __init__(self, calls_per_minute):
        self.calls_per_minute = calls_per_minute
        self.timestamps = []

    async def wait_if_needed(self):
        now = time.time()
        # 移除 1 分钟外的记录
        self.timestamps = [t for t in self.timestamps if now - t < 60]

        if len(self.timestamps) >= self.calls_per_minute:
            sleep_time = 60 - (now - self.timestamps[0])
            await asyncio.sleep(sleep_time)

        self.timestamps.append(time.time())

3. 敏感信息加密

使用 keyring 库安全存储 API 密钥:

import keyring

def store_api_key(key):
    keyring.set_password('chatgpt_cli', 'api_key', key)

def get_api_key():
    return keyring.get_password('chatgpt_cli', 'api_key')

避坑指南

  1. Token 计算误差
  2. 实际使用的 token 数可能比预估多 10-15%
  3. 解决方案:使用 tiktoken 库精确计算
import tiktoken

def count_tokens(text, model='gpt-3.5-turbo'):
    encoding = tiktoken.encoding_for_model(model)
    return len(encoding.encode(text))
  1. 流式输出刷新问题
  2. 直接打印会导致终端闪烁
  3. 解决方案:使用 rich 的 Live 显示
from rich.live import Live

async def stream_response(response):
    with Live(console=console) as live:
        full_content = ''
        async for chunk in response.content:
            full_content += chunk.decode()
            live.update(Markdown(full_content))
  1. 代理配置技巧
  2. 国内用户需要配置代理
  3. 最佳实践:支持环境变量和配置文件两种方式
proxy = os.getenv('OPENAI_PROXY') or config.get('DEFAULT', 'proxy', fallback=None)
if proxy:
    connector = aiohttp.TCPConnector(proxy=proxy)
    session = aiohttp.ClientSession(connector=connector)

代码规范

所有代码遵循 PEP8 规范,关键函数包含类型标注和文档字符串:

def process_response(response: dict) -> str:
    """
    处理 OpenAI API 响应,提取回复内容

    Args:
        response: OpenAI API 返回的 JSON 响应

    Returns:
        模型生成的文本内容
    """choices = response.get('choices', [])
    if not choices:
        raise ValueError('无效的 API 响应')
    return choices[0]['message']['content']

延伸思考

这个基础 CLI 工具可以进一步扩展:

  1. 自定义 prompt 模板
  2. 支持保存常用 prompt 模板
  3. 实现快捷调用复杂查询

  4. 代码执行沙箱

  5. 集成 Python exec 环境
  6. 允许直接执行模型生成的代码

  7. 多模态支持

  8. 结合 DALL·E API
  9. 支持图片生成和显示

  10. 插件系统

  11. 开发插件接口
  12. 支持第三方功能扩展

通过这个项目,我们不仅构建了一个实用的开发工具,还深入理解了异步编程、CLI 开发和 API 集成的关键技术点。希望这个实现方案能为你的开发工作流带来实质性的效率提升。

正文完
 0
评论(没有评论)