From 15176e2eaf21153fa3d9dc8d36951a1405d9bdc6 Mon Sep 17 00:00:00 2001 From: jinye_huang Date: Wed, 23 Apr 2025 16:18:02 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E5=86=99=E4=BA=86=E6=B5=81=E5=BC=8F?= =?UTF-8?q?=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 14 + core/__pycache__/ai_agent.cpython-312.pyc | Bin 18053 -> 30059 bytes core/__pycache__/contentGen.cpython-312.pyc | Bin 18887 -> 17387 bytes core/ai_agent.py | 270 +++++++++++++++++++- core/contentGen.py | 226 ++++++++-------- docs/streaming.md | 209 +++++++++++++++ examples/README.md | 25 ++ examples/README_streaming.md | 103 ++++++++ examples/test_stream.py | 268 +++++++++++-------- 9 files changed, 884 insertions(+), 231 deletions(-) create mode 100644 docs/streaming.md create mode 100644 examples/README_streaming.md diff --git a/README.md b/README.md index b78cdf3..e634a23 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,20 @@ - **模块化设计**:核心功能(配置加载、提示词管理、AI交互、选题、内容生成、海报制作)分离,方便维护和扩展 - **配置驱动**:通过配置文件集中管理所有运行参数 +## 新功能: 流式输出处理 + +TravelContentCreator 现已支持三种流式输出处理方法,提供了更灵活的 AI 文本生成体验: + +- **同步流式响应**: 使用流式 API 但返回完整响应 +- **回调式流式响应**: 通过回调函数处理每个文本块 +- **异步流式响应**: 使用异步生成器返回文本流 + +这些功能大大提升了长文本生成的用户体验和系统响应性。 + +详细文档请参阅: +- [流式处理文档](docs/streaming.md) +- [流式处理演示](examples/test_stream.py) + ## 快速开始 ### 1. 环境准备 diff --git a/core/__pycache__/ai_agent.cpython-312.pyc b/core/__pycache__/ai_agent.cpython-312.pyc index d9f9d5461f0777dfb8f1277d9bb357c293d840e4..ce80f7e0f6fd7323440b09b1bceb1605f14605c8 100644 GIT binary patch delta 11625 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 delta 2974 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 diff --git a/core/__pycache__/contentGen.cpython-312.pyc b/core/__pycache__/contentGen.cpython-312.pyc index 53c9a07c6267c2dc98f3bf36cd762b7ef7dd9bb3..62f1d66a89ee992350c09f79fb826fbcf18f2d95 100644 GIT binary patch delta 6240 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 self.stream_chunk_timeout: + callback_fn("", is_last=True, is_timeout=True, is_error=False, error=None) + raise Timeout(f"No chunk received for {self.stream_chunk_timeout} seconds.") + + chunk = next(chunk_iterator) + last_chunk_time = time.time() # 成功接收块后重置计时器 + + content = "" + is_last = False + + if chunk.choices and chunk.choices[0].delta and chunk.choices[0].delta.content: + content = chunk.choices[0].delta.content + full_response += content + + if chunk.choices and chunk.choices[0].finish_reason == 'stop': + is_last = True + + # 调用回调函数处理块 + callback_fn(content, is_last=is_last, is_timeout=False, is_error=False, error=None) + + if is_last: + logging.info("Stream with callback finished normally.") + return full_response # 成功完成,返回完整响应 + + except StopIteration: + # 正常结束流 + callback_fn("", is_last=True, is_timeout=False, is_error=False, error=None) + logging.info("Stream iterator exhausted normally.") + return full_response + + except Timeout as e: + logging.warning(f"Stream chunk timeout: {e}") + last_exception = e + # 超时信息已通过回调传递,此处不需要再次调用回调 + except (APITimeoutError, APIConnectionError, RateLimitError) as e: + logging.warning(f"API error during streaming with callback: {type(e).__name__} - {e}") + callback_fn("", is_last=True, is_timeout=False, is_error=True, error=str(e)) + last_exception = e + except Exception as e: + logging.error(f"Unexpected error during streaming with callback: {traceback.format_exc()}") + callback_fn("", is_last=True, is_timeout=False, is_error=True, error=str(e)) + last_exception = e + + # 执行重试逻辑 + retries += 1 + if retries < self.max_retries: + retry_msg = f"将在 {backoff_time:.2f} 秒后重试..." + logging.info(f"Retrying stream in {backoff_time} seconds...") + callback_fn(f"\n[{retry_msg}]\n", is_last=False, is_timeout=False, is_error=False, error=None) + time.sleep(backoff_time + random.uniform(0, 1)) # 添加随机抖动 + backoff_time = min(backoff_time * 2, MAX_BACKOFF) + else: + error_msg = f"API call failed after {self.max_retries} retries: {str(last_exception)}" + logging.error(error_msg) + callback_fn(f"\n[{error_msg}]\n", is_last=True, is_timeout=False, is_error=True, error=str(last_exception)) + return full_response # 返回已收集的部分响应 + + except Exception as e: + logging.error(f"Error setting up stream with callback: {traceback.format_exc()}") + callback_fn("", is_last=True, is_timeout=False, is_error=True, error=str(e)) + return f"[生成内容失败: {str(e)}]" + + # 作为安全措施 + error_msg = "超出最大重试次数" + logging.error(error_msg) + callback_fn(f"\n[{error_msg}]\n", is_last=True, is_timeout=False, is_error=True, error=error_msg) + return full_response + + async def async_generate_text_stream(self, system_prompt, user_prompt, temperature=0.7, top_p=0.9, presence_penalty=0.0): + """异步生成文本流 + + Args: + system_prompt: 系统提示词 + user_prompt: 用户提示词 + temperature: 温度参数 + top_p: 核采样参数 + presence_penalty: 存在惩罚参数 + + Yields: + str: 生成的文本块 + + Raises: + Exception: 如果API调用在所有重试后失败 + """ + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt}, + ] + logging.info(f"Asynchronously generating text stream with model: {self.model_name}") + + retries = 0 + backoff_time = INITIAL_BACKOFF + last_exception = None + + while retries < self.max_retries: + try: + logging.debug(f"Async attempt {retries + 1}/{self.max_retries} to generate text stream.") + # 创建新的客户端用于异步操作 + async_client = OpenAI( + api_key=self.api, + base_url=self.base_url, + timeout=self.timeout + ) + + stream = await async_client.chat.completions.create( + model=self.model_name, + messages=messages, + temperature=temperature, + top_p=top_p, + presence_penalty=presence_penalty, + stream=True, + timeout=self.timeout + ) + + last_chunk_time = time.time() + try: + async for chunk in stream: + # 检查上次接收块以来的超时 + current_time = time.time() + if current_time - last_chunk_time > self.stream_chunk_timeout: + raise Timeout(f"No chunk received for {self.stream_chunk_timeout} seconds.") + + last_chunk_time = current_time # 成功接收块后重置计时器 + + if chunk.choices and chunk.choices[0].delta and chunk.choices[0].delta.content: + content = chunk.choices[0].delta.content + yield content + + logging.info("Async stream finished normally.") + return # 成功完成 + + except AsyncTimeoutError as e: + logging.warning(f"Async stream timeout: {e}") + last_exception = e + except Timeout as e: + logging.warning(f"Async stream chunk timeout: {e}") + last_exception = e + except Exception as e: + logging.error(f"Error during async streaming: {traceback.format_exc()}") + last_exception = e + + # 执行重试逻辑 + retries += 1 + if retries < self.max_retries: + logging.info(f"Retrying async stream in {backoff_time} seconds...") + await asyncio.sleep(backoff_time + random.uniform(0, 1)) # 使用异步睡眠 + backoff_time = min(backoff_time * 2, MAX_BACKOFF) + else: + logging.error(f"Async API call failed after {self.max_retries} retries.") + raise last_exception or Exception("Async stream generation failed after max retries.") + + except (Timeout, AsyncTimeoutError, APITimeoutError, APIConnectionError, RateLimitError) as e: + retries += 1 + last_exception = e + logging.warning(f"Async attempt {retries}/{self.max_retries} failed: {type(e).__name__} - {e}") + if retries < self.max_retries: + logging.info(f"Retrying async stream in {backoff_time} seconds...") + await asyncio.sleep(backoff_time + random.uniform(0, 1)) # 使用异步睡眠 + backoff_time = min(backoff_time * 2, MAX_BACKOFF) + else: + logging.error(f"Async API call failed after {self.max_retries} retries.") + raise last_exception + except Exception as e: + logging.error(f"Unexpected error setting up async stream: {traceback.format_exc()}") + raise e # 立即重新引发意外错误 + + # 作为安全措施 + logging.error("Exited async stream generation loop unexpectedly.") + raise last_exception or Exception("Async stream generation failed.") + + async def async_work_stream(self, system_prompt, user_prompt, file_folder, temperature, top_p, presence_penalty): + """异步完整工作流程:读取文件夹(如果提供),然后生成文本流""" + logging.info(f"Starting async 'work_stream' process. File folder: {file_folder}") + if file_folder: + logging.info(f"Reading context from folder: {file_folder}") + context = self.read_folder(file_folder) + if context: + user_prompt = f"{user_prompt.strip()}\n\n--- 参考资料 ---\n{context.strip()}" + else: + logging.warning(f"Folder {file_folder} provided but no content read.") + + logging.info("Calling async_generate_text_stream...") + return self.async_generate_text_stream(system_prompt, user_prompt, temperature, top_p, presence_penalty) def work_stream(self, system_prompt, user_prompt, file_folder, temperature, top_p, presence_penalty): """完整的工作流程(流式):读取文件夹(如果提供),然后生成文本流""" @@ -333,6 +577,6 @@ class AI_Agent(): else: logging.warning(f"Folder {file_folder} provided but no content read.") - logging.info("Calling generate_text_stream...") - return self.generate_text_stream(system_prompt, user_prompt, temperature, top_p, presence_penalty) - # --- End Added Streaming Methods --- \ No newline at end of file + full_response = self.generate_text_stream(system_prompt, user_prompt, temperature, top_p, presence_penalty) + return full_response + # --- End Streaming Methods --- \ No newline at end of file diff --git a/core/contentGen.py b/core/contentGen.py index dbd2f75..7c17e00 100644 --- a/core/contentGen.py +++ b/core/contentGen.py @@ -45,6 +45,11 @@ class ContentGenerator: self.top_p = 0.8 self.presence_penalty = 1.2 + # 设置日志 + logging.basicConfig(level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') + self.logger = logging.getLogger(__name__) + def load_infomation(self, info_directory_path): """ @@ -193,142 +198,149 @@ class ContentGenerator: 返回: 生成的海报内容 """ - if system_prompt is None: - system_prompt = """ - 你是一名资深海报设计师,有丰富的爆款海报设计经验,你现在要为旅游景点做宣传,在小红书上发布大量宣传海报。你的主要工作目标有2个: - 1、你要根据我给你的图片描述和笔记推文内容,设计图文匹配的海报。 - 2、为海报设计文案,文案的<第一个小标题>和<第二个小标题>之间你需要检查是否逻辑关系合理,你将通过先去生成<第二个小标题>关于景区亮点的部分,再去综合判断<第一个小标题>应该如何搭配组合更符合两个小标题的逻辑再生成<第一个小标题>。 - - 其中,生成三类标题文案的通用性要求如下: - 1、生成的<大标题>字数必须小于8个字符 - 2、生成的<第一个小标题>字数和<第二个小标题>字数,两者都必须小8个字符 - 3、标题和文案都应符合中国社会主义核心价值观 - - 接下来先开始生成<大标题>部分,由于海报是用来宣传旅游景点,生成的海报<大标题>必须使用以下8种格式之一: - ①地名+景点名(例如福建厦门鼓浪屿/厦门鼓浪屿); - ②地名+景点名+plog; - ③拿捏+地名+景点名; - ④地名+景点名+攻略; - ⑤速通+地名+景点名 - ⑥推荐!+地名+景点名 - ⑦勇闯!+地名+景点名 - ⑧收藏!+地名+景点名 - 你需要随机挑选一种格式生成对应景点的文案,但是格式除了上面8种不可以有其他任何格式;同时尽量保证每一种格式出现的频率均衡。 - 接下来先去生成<第二个小标题>,<第二个小标题>文案的创作必须遵循以下原则: - 请根据笔记内容和图片识别,用极简的文字概括这篇笔记和图片中景点的特色亮点,其中你可以参考以下词汇进行创作,这段文案字数控制6-8字符以内; - - 特色亮点可能会出现的词汇不完全举例:非遗、古建、绝佳山水、祈福圣地、研学圣地、解压天堂、中国小瑞士、秘境竹筏游等等类型词汇 - - 接下来再去生成<第一个小标题>,<第一个小标题>文案的创作必须遵循以下原则: - 这部分文案创作公式有5种,分别为: - ①<受众人群画像>+<痛点词> - ②<受众人群画像> - ③<痛点词> - ④<受众人群画像>+ | +<痛点词> - ⑤<痛点词>+ | +<受众人群画像> - 请你根据实际笔记内容,结合这部分文案创作公式,需要结合<受众人群画像>和<痛点词>时,必须根据<第二个小标题>的景点特征和所对应的完整笔记推文内容主旨,特征挑选对应<受众人群画像>和<痛点词>。 - - 我给你提供受众人群画像库和痛点词库如下: - 1、受众人群画像库:情侣党、亲子游、合家游、银发族、亲子研学、学生党、打工人、周边游、本地人、穷游党、性价比、户外人、美食党、出片 - 2、痛点词库:3天2夜、必去、看了都哭了、不能错过、一定要来、问爆了、超全攻略、必打卡、强推、懒人攻略、必游榜、小众打卡、狂喜等等。 - - 你需要为每个请求至少生成{poster_num}个海报设计。请使用JSON格式输出结果,结构如下: - - ```json + full_response = "" + timeout = 60 # 请求超时时间(秒) + + if not system_prompt: + # 使用默认系统提示词 + system_prompt = f""" + 你是一个专业的文案处理专家,擅长从文章中提取关键信息并生成吸引人的标题和简短描述。 + 现在,我需要你根据提供的文章内容,生成{poster_num}个海报的文案配置。 + + 每个配置包含: + 1. main_title:主标题,简短有力,突出景点特点 + 2. texts:两句简短文本,每句不超过15字,描述景点特色或游玩体验 + + 以JSON数组格式返回配置,示例: [ - { - "index": 1, - "main_title": "主标题内容", - "texts": ["第一个小标题", "第二个小标题"] - }, - { - "index": 2, - "main_title": "主标题内容", - "texts": ["第一个小标题", "第二个小标题"] - } - // ... 更多海报 + {{ + "main_title": "泰宁古城", + "texts": ["千年古韵","匠心独运"] + }}, + ... ] - ``` - - 确保生成的数量与用户要求的数量一致。只生成上述JSON格式内容,不要有其他任何额外内容。 + + 仅返回JSON数据,不需要任何额外解释。确保生成的标题和文本能够准确反映文章提到的景点特色。 """ - user_content = f""" - 海报数量:{poster_num}; - 景区介绍:{self.add_description}; - 推文内容:{tweet_content}; - """ - - # 最终响应内容 - full_response = "" + if self.add_description: + # 创建用户内容,包括info信息和tweet_content + user_content = f""" + 以下是需要你处理的信息: + + 关于景点的描述: + {self.add_description} + + 推文内容: + {tweet_content} + + 请根据这些信息,生成{poster_num}个海报文案配置,以JSON数组格式返回。 + 确保主标题(main_title)简短有力,每个text不超过15字,并能准确反映景点特色。 + """ + else: + # 仅使用tweet_content + user_content = f""" + 以下是需要你处理的推文内容: + {tweet_content} + + 请根据这些信息,生成{poster_num}个海报文案配置,以JSON数组格式返回。 + 确保主标题(main_title)简短有力,每个text不超过15字,并能准确反映景点特色。 + """ + + self.logger.info(f"正在生成{poster_num}个海报文案配置") # 创建临时客户端 temp_client = self._create_temp_client() - # 如果创建客户端失败,直接使用备用方案 - if temp_client is None: - print("创建OpenAI客户端失败,使用备用方案生成内容") - return 404 - else: - # 添加重试机制 + if temp_client: + # 重试逻辑 for retry in range(max_retries): try: - print(f"尝试连接API (尝试 {retry+1}/{max_retries})...") + self.logger.info(f"尝试生成内容 (尝试 {retry+1}/{max_retries})") - # 计算退避时间(指数退避策略):0, 2, 4, 8, 16...秒 - if retry > 0: - backoff_time = min(2 ** (retry - 1) * 2, 30) # 最大等待30秒 - print(f"等待 {backoff_time} 秒后重试...") - time.sleep(backoff_time) + # 定义流式响应处理回调函数 + def handle_stream_chunk(chunk, is_last=False, is_timeout=False, is_error=False, error=None): + nonlocal full_response + + if chunk: + full_response += chunk + # 实时输出到控制台 + print(chunk, end="", flush=True) + + if is_last: + print("\n") # 输出完成后换行 + if is_timeout: + print("警告: 响应流超时") + if is_error: + print(f"错误: {error}") - # 设置超时时间随重试次数递增 - timeout = 30 + (retry * 30) # 30, 60, 90, ...秒 - - chat_response = temp_client.chat.completions.create( - model=self.model_name, - messages=[ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": user_content} - ], - stream=True, - temperature=self.temperature, - top_p=self.top_p, - presence_penalty=self.presence_penalty, - timeout=timeout # 设置请求超时时间 + # 使用AI_Agent的新回调方式 + from core.ai_agent import AI_Agent + ai_agent = AI_Agent( + self.api_base_url, + self.model_name, + self.api_key, + timeout=timeout, + max_retries=max_retries, + stream_chunk_timeout=30 # 流式块超时时间 ) - # 获取响应内容 - for chunk in chat_response: - if chunk.choices and len(chunk.choices) > 0 and chunk.choices[0].delta.content is not None: - content = chunk.choices[0].delta.content - full_response += content - print(content, end="", flush=True) - if chunk.choices and len(chunk.choices) > 0 and chunk.choices[0].finish_reason == "stop": - break - - print("\n") # 输出完成后换行 - - # 成功获取响应,跳出重试循环 - break + # 使用回调方式处理流式响应 + try: + full_response = ai_agent.generate_text_stream_with_callback( + system_prompt, + user_content, + handle_stream_chunk, + temperature=self.temperature, + top_p=self.top_p, + presence_penalty=self.presence_penalty + ) + + # 如果成功生成内容,跳出重试循环 + ai_agent.close() + break + + except Exception as e: + error_msg = str(e) + self.logger.error(f"AI生成错误: {error_msg}") + ai_agent.close() + + # 继续重试逻辑 + if retry + 1 >= max_retries: + self.logger.warning("已达到最大重试次数,使用备用方案...") + # 生成备用内容 + full_response = self._generate_fallback_content(poster_num) + else: + self.logger.info(f"将在稍后重试,还剩 {max_retries - retry - 1} 次重试机会") except Exception as e: error_msg = str(e) - print(f"API连接错误 (尝试 {retry+1}/{max_retries}): {error_msg}") + self.logger.error(f"API连接错误 (尝试 {retry+1}/{max_retries}): {error_msg}") # 如果已经达到最大重试次数 if retry + 1 >= max_retries: - print("已达到最大重试次数,使用备用方案...") + self.logger.warning("已达到最大重试次数,使用备用方案...") # 生成备用内容(简单模板) full_response = self._generate_fallback_content(poster_num) else: - print(f"将在稍后重试,还剩 {max_retries - retry - 1} 次重试机会") + self.logger.info(f"将在稍后重试,还剩 {max_retries - retry - 1} 次重试机会") # 关闭临时客户端 self._close_client(temp_client) - # 生成时间戳 return full_response + def _generate_fallback_content(self, poster_num): + """生成备用内容,当API调用失败时使用""" + self.logger.info("生成备用内容") + default_configs = [] + for i in range(poster_num): + default_configs.append({ + "main_title": f"景点风光 {i+1}", + "texts": ["自然美景", "人文体验"] + }) + return json.dumps(default_configs, ensure_ascii=False) + def save_result(self, full_response): """ 保存生成结果到文件 diff --git a/docs/streaming.md b/docs/streaming.md new file mode 100644 index 0000000..7f1b1f2 --- /dev/null +++ b/docs/streaming.md @@ -0,0 +1,209 @@ +# 流式输出处理功能 + +TravelContentCreator 现在支持三种不同的流式输出处理方法,让您能够更灵活地处理 AI 模型生成的文本内容。这些方法都在 `AI_Agent` 类中实现,可以根据不同的使用场景进行选择。 + +## 为什么需要流式处理? + +流式处理(Streaming)相比于传统的一次性返回完整响应的方式有以下优势: + +1. **实时性**:内容生成的同时即可开始处理,无需等待完整响应 +2. **用户体验更好**:可以实现"打字机效果",让用户看到文本逐步生成的过程 +3. **更早检测错误**:可以在响应生成过程中及早发现问题 +4. **长文本处理更高效**:特别适合生成较长的内容,避免长时间等待 + +## 流式处理方法 + +`AI_Agent` 类提供了三种不同模式的流式处理方法: + +### 1. 同步流式响应 (generate_text_stream) + +这种方法虽然使用了流式 API 连接,但会将所有的输出整合后一次性返回,适合简单的 API 调用。 + +```python +def generate_text_stream(self, system_prompt, user_prompt, temperature, top_p, presence_penalty): + """ + 生成文本内容(使用流式API但返回完整响应) + + Args: + system_prompt: 系统提示词 + user_prompt: 用户提示词 + temperature: 温度参数 + top_p: 核采样参数 + presence_penalty: 存在惩罚参数 + + Returns: + str: 完整的生成文本 + """ +``` + +使用示例: + +```python +agent = AI_Agent(base_url, model_name, api_key, timeout=30, stream_chunk_timeout=10) +result = agent.generate_text_stream(system_prompt, user_prompt, 0.7, 0.9, 0.0) +print(result) # 输出完整的生成结果 +``` + +### 2. 回调式流式响应 (generate_text_stream_with_callback) + +这种方法使用回调函数来处理流中的每个文本块,非常适合实时显示、分析或保存过程数据,更加灵活。 + +```python +def generate_text_stream_with_callback(self, system_prompt, user_prompt, + callback_fn, temperature=0.7, top_p=0.9, + presence_penalty=0.0): + """ + 生成文本流并通过回调函数处理每个块 + + Args: + system_prompt: 系统提示词 + user_prompt: 用户提示词 + callback_fn: 处理每个文本块的回调函数,接收(content, is_last, is_timeout, is_error, error)参数 + temperature: 温度参数 + top_p: 核采样参数 + presence_penalty: 存在惩罚参数 + + Returns: + str: 完整的响应文本 + """ +``` + +回调函数应符合以下格式: + +```python +def my_callback(content, is_last=False, is_timeout=False, is_error=False, error=None): + """ + 处理流式响应的回调函数 + + Args: + content: 文本块内容 + is_last: 是否为最后一个块 + is_timeout: 是否发生超时 + is_error: 是否发生错误 + error: 错误信息 + """ + if content: + print(content, end="", flush=True) # 实时打印 + + # 处理特殊情况 + if is_last: + print("\n完成生成") + if is_timeout: + print("警告: 响应流超时") + if is_error: + print(f"错误: {error}") +``` + +使用示例: + +```python +agent = AI_Agent(base_url, model_name, api_key, timeout=30, stream_chunk_timeout=10) +result = agent.generate_text_stream_with_callback( + system_prompt, + user_prompt, + my_callback, # 传入回调函数 + temperature=0.7, + top_p=0.9, + presence_penalty=0.0 +) +``` + +### 3. 异步流式响应 (async_generate_text_stream) + +这种方法基于 `asyncio`,返回一个异步生成器,非常适合与其他异步操作集成,例如在异步网络应用中使用。 + +```python +async def async_generate_text_stream(self, system_prompt, user_prompt, temperature=0.7, top_p=0.9, presence_penalty=0.0): + """ + 异步生成文本流 + + Args: + system_prompt: 系统提示词 + user_prompt: 用户提示词 + temperature: 温度参数 + top_p: 核采样参数 + presence_penalty: 存在惩罚参数 + + Yields: + str: 生成的文本块 + + Raises: + Exception: 如果API调用在所有重试后失败 + """ +``` + +使用示例: + +```python +async def demo_async_stream(): + agent = AI_Agent(base_url, model_name, api_key, timeout=30, stream_chunk_timeout=10) + + full_response = "" + try: + # 使用异步生成器 + async for chunk in agent.async_generate_text_stream( + system_prompt, + user_prompt, + temperature=0.7, + top_p=0.9, + presence_penalty=0.0 + ): + print(chunk, end="", flush=True) # 实时显示 + full_response += chunk + + # 这里可以同时执行其他异步操作 + # await some_other_async_operation() + + except Exception as e: + print(f"错误: {e}") + finally: + agent.close() + +# 在异步环境中运行 +asyncio.run(demo_async_stream()) +``` + +## 超时处理 + +所有流式处理方法都支持两种超时设置: + +1. **全局请求超时**:控制整个API请求的最大持续时间 +2. **流块超时**:控制接收连续两个数据块之间的最大等待时间 + +在创建 `AI_Agent` 实例时设置: + +```python +agent = AI_Agent( + base_url="your_api_base_url", + model_name="your_model_name", + api="your_api_key", + timeout=60, # 整体请求超时(秒) + max_retries=3, # 最大重试次数 + stream_chunk_timeout=10 # 流块超时(秒) +) +``` + +## 完整示例 + +我们提供了一个完整的示例脚本,演示所有三种流式处理方法的使用: + +``` +examples/test_stream.py +``` + +运行方式: + +```bash +cd TravelContentCreator +python examples/test_stream.py +``` + +这将依次演示三种流式处理方法,并展示它们的输出和性能差异。 + +## 在WebUI中的应用 + +TravelContentCreator的WebUI已经集成了基于回调的流式处理,实现了生成内容的实时显示,大大提升了用户体验,特别是在生成长篇内容时。 + +## 在自定义项目中使用 + +如果您想在自己的项目中使用这些流式处理功能,只需导入 `AI_Agent` 类并按照上述示例使用相应的方法即可。所有流式处理方法都内置了完善的错误处理和重试机制,提高了生产环境中的稳定性。 \ No newline at end of file diff --git a/examples/README.md b/examples/README.md index 0cfa948..d7a8354 100644 --- a/examples/README.md +++ b/examples/README.md @@ -156,6 +156,31 @@ python examples/test_stream.py - 演示实时输出文本块的处理方式 - 说明如何收集完整响应和估计token数 +## 流式处理演示 (test_stream.py) + +`test_stream.py` 是一个演示脚本,展示了AI_Agent类中新增的三种不同流式输出处理方法: + +1. **同步流式响应** (generate_text_stream) - 使用流式API但返回完整响应 +2. **回调式流式响应** (generate_text_stream_with_callback) - 通过回调函数处理每个文本块 +3. **异步流式响应** (async_generate_text_stream) - 使用异步生成器返回文本流 + +### 运行演示 + +```bash +cd TravelContentCreator +python examples/test_stream.py +``` + +演示脚本会依次执行这三种方法,并展示它们的输出和性能差异。 + +### 更多信息 + +关于流式处理功能的详细文档,请参阅: + +``` +docs/streaming.md +``` + ## 其他示例 `generate_poster.py` 是一个简化的海报生成示例,主要用于快速测试特定图片的海报效果。 diff --git a/examples/README_streaming.md b/examples/README_streaming.md new file mode 100644 index 0000000..9953513 --- /dev/null +++ b/examples/README_streaming.md @@ -0,0 +1,103 @@ +# 流式处理功能说明 + +本文档介绍了Travel Content Creator中新增的三种流式输出处理方式,以及如何使用它们来优化内容生成体验。 + +## 新增的流式处理方式 + +我们在`AI_Agent`类中实现了三种不同的流式处理方式: + +1. **同步流式响应** (`generate_text_stream`) + - 已修改为返回完整响应,不再是生成器 + - 内部仍使用流式请求以获得更好的超时控制 + - 适用于简单的API调用场景 + +2. **基于回调的流式响应** (`generate_text_stream_with_callback`) + - 通过回调函数处理每个文本块 + - 可在回调中实现实时显示、分析或保存 + - 最灵活的选项,适合需要自定义处理流程的场景 + +3. **异步流式响应** (`async_generate_text_stream`) + - 基于`asyncio`的异步生成器 + - 适用于需要与其他异步操作集成的场景 + - 可在保持响应性的同时处理长时间运行的请求 + +## 演示脚本 + +我们提供了一个示例脚本`test_stream.py`,演示了如何使用这三种流式处理方式: + +```bash +# 从项目根目录运行 +python examples/test_stream.py +``` + +## 回调函数示例 + +以下是使用回调函数处理流式响应的例子: + +```python +def handle_chunk(chunk, is_last=False, is_timeout=False, is_error=False, error=None): + # 处理文本块 + if chunk: + print(chunk, end="", flush=True) # 实时显示 + + # 处理结束状态 + if is_last: + if is_timeout: + print("\n流式响应超时") + if is_error: + print(f"\n发生错误: {error}") + +# 使用回调函数进行流式处理 +response = agent.generate_text_stream_with_callback( + system_prompt, + user_prompt, + handle_chunk, # 传入回调函数 + temperature=0.7 +) +``` + +## 异步流式处理示例 + +以下是使用异步方式处理流式响应的例子: + +```python +async def process_stream(): + async for chunk in agent.async_generate_text_stream( + system_prompt, + user_prompt, + temperature=0.7 + ): + print(chunk, end="", flush=True) # 实时显示 + + # 可以同时执行其他异步操作 + await other_async_task() + +# 在异步环境中运行 +asyncio.run(process_stream()) +``` + +## 超时处理 + +所有流式处理方法都支持超时控制: + +1. **全局请求超时**:控制整个请求的最长等待时间 +2. **流式块超时**:控制两个连续文本块之间的最长等待时间 + +这些参数可以在创建`AI_Agent`实例时设置: + +```python +agent = AI_Agent( + api_url="http://localhost:8000/v1/", + model="qwen", + api_key="EMPTY", + timeout=30, # 全局请求超时(秒) + stream_chunk_timeout=10 # 流式块超时(秒) +) +``` + +## 注意事项 + +1. 新的流式处理方法内置了重试机制和错误处理 +2. 回调方式提供了最佳的灵活性和控制 +3. 异步方式最适合需要保持UI响应性的应用 +4. 所有方法都会在完成时自动关闭流式请求 \ No newline at end of file diff --git a/examples/test_stream.py b/examples/test_stream.py index 6beba94..8489f62 100644 --- a/examples/test_stream.py +++ b/examples/test_stream.py @@ -1,129 +1,175 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +""" +测试AI_Agent的流式处理方法 + +此脚本演示TravelContentCreator中AI_Agent类的三种流式输出处理方法: +1. 同步流式响应 (generate_text_stream) +2. 回调式流式响应 (generate_text_stream_with_callback) +3. 异步流式响应 (async_generate_text_stream) +""" + import os import sys -import json +import asyncio import time -import logging +from pathlib import Path -# Determine the project root directory (assuming examples/ is one level down) -PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -if PROJECT_ROOT not in sys.path: - sys.path.append(PROJECT_ROOT) +# 添加项目根目录到Python路径 +project_root = str(Path(__file__).parent.parent) +if project_root not in sys.path: + sys.path.insert(0, project_root) -# Now import from core -try: - from core.ai_agent import AI_Agent -except ImportError as e: - logging.critical(f"Failed to import AI_Agent. Ensure '{PROJECT_ROOT}' is in sys.path and core/ai_agent.py exists. Error: {e}") - sys.exit(1) +from core.ai_agent import AI_Agent -def load_config(config_path): - """Loads configuration from a JSON file.""" - try: - with open(config_path, 'r', encoding='utf-8') as f: - config = json.load(f) - logging.info(f"Config loaded successfully from {config_path}") - return config - except FileNotFoundError: - logging.error(f"Error: Configuration file not found at {config_path}") - return None - except json.JSONDecodeError: - logging.error(f"Error: Could not decode JSON from {config_path}") - return None - except Exception as e: - logging.exception(f"An unexpected error occurred loading config {config_path}:") - return None +# 示例提示词 +SYSTEM_PROMPT = """你是一个专业的旅游内容创作助手,请根据用户的提示生成相关内容。""" +USER_PROMPT = """请为我生成一篇关于福建泰宁古城的旅游攻略,包括著名景点、美食推荐和最佳游玩季节。字数控制在300字以内。""" -def main(): - # --- Basic Logging Setup --- - logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s', - datefmt='%Y-%m-%d %H:%M:%S' +def print_separator(title): + """打印分隔线和标题""" + print("\n" + "="*50) + print(f" {title} ".center(50, "=")) + print("="*50 + "\n") + +def demo_sync_stream(): + """演示同步流式响应方法""" + print_separator("同步流式响应 (generate_text_stream)") + + # 创建AI_Agent实例 + agent = AI_Agent( + base_url="vllm", # 使用本地vLLM服务 + model_name="qwen2-7b-instruct", # 或其他您配置的模型名称 + api="EMPTY", # vLLM不需要API key + timeout=60, # 整体请求超时时间(秒) + stream_chunk_timeout=10 # 流式块超时时间(秒) ) - # --- End Logging Setup --- + + print("开始生成内容...") + start_time = time.time() + + # 使用同步流式方法 + result = agent.generate_text_stream( + SYSTEM_PROMPT, + USER_PROMPT, + temperature=0.7, + top_p=0.9, + presence_penalty=0.0 + ) + + end_time = time.time() + + print(f"\n\n完整生成内容:\n{result}") + print(f"\n生成完成! 耗时: {end_time - start_time:.2f}秒") + + # 关闭agent + agent.close() - logging.info("Starting AI Agent Stream Test...") - - # Load configuration (adjust path relative to this script) - config_path = os.path.join(PROJECT_ROOT, "poster_gen_config.json") - config = load_config(config_path) - if config is None: - logging.critical("Failed to load configuration. Exiting test.") - sys.exit(1) - - # Example Prompts - system_prompt = "你是一个乐于助人的AI助手,擅长写短篇故事。" - user_prompt = "请写一个关于旅行机器人的短篇故事,它在一个充满异国情调的星球上发现了新的生命形式。" - - ai_agent = None - try: - # --- Extract AI Agent parameters from config --- - ai_api_url = config.get("api_url") - ai_model = config.get("model") - ai_api_key = config.get("api_key") - request_timeout = config.get("request_timeout", 30) - max_retries = config.get("max_retries", 3) - stream_chunk_timeout = config.get("stream_chunk_timeout", 60) # Get stream chunk timeout +def demo_callback_stream(): + """演示回调式流式响应方法""" + print_separator("回调式流式响应 (generate_text_stream_with_callback)") + + # 创建AI_Agent实例 + agent = AI_Agent( + base_url="vllm", + model_name="qwen2-7b-instruct", + api="EMPTY", + timeout=60, + stream_chunk_timeout=10 + ) + + # 定义回调函数 + def my_callback(content, is_last=False, is_timeout=False, is_error=False, error=None): + """处理流式响应的回调函数""" + if content: + # 实时打印内容,不换行 + print(content, end="", flush=True) - # Check for required AI params - if not all([ai_api_url, ai_model, ai_api_key]): - logging.critical("Missing required AI configuration (api_url, model, api_key) in config. Exiting test.") - sys.exit(1) - # --- End Extract AI Agent params --- + if is_last: + print("\n") + if is_timeout: + print("警告: 响应流超时") + if is_error: + print(f"错误: {error}") + + print("开始生成内容...") + start_time = time.time() + + # 使用回调式流式方法 + result = agent.generate_text_stream_with_callback( + SYSTEM_PROMPT, + USER_PROMPT, + my_callback, + temperature=0.7, + top_p=0.9, + presence_penalty=0.0 + ) + + end_time = time.time() + print(f"\n生成完成! 耗时: {end_time - start_time:.2f}秒") + + # 关闭agent + agent.close() - logging.info("Initializing AI Agent for stream test...") - # Initialize AI_Agent using extracted parameters - ai_agent = AI_Agent( - api_url=ai_api_url, # Use extracted var - model=ai_model, # Use extracted var - api_key=ai_api_key, # Use extracted var - timeout=request_timeout, - max_retries=max_retries, - stream_chunk_timeout=stream_chunk_timeout # Pass it here +async def demo_async_stream(): + """演示异步流式响应方法""" + print_separator("异步流式响应 (async_generate_text_stream)") + + # 创建AI_Agent实例 + agent = AI_Agent( + base_url="vllm", + model_name="qwen2-7b-instruct", + api="EMPTY", + timeout=60, + stream_chunk_timeout=10 + ) + + print("开始生成内容...") + start_time = time.time() + full_response = "" + + # 使用异步流式方法 + try: + async_stream = agent.async_generate_text_stream( + SYSTEM_PROMPT, + USER_PROMPT, + temperature=0.7, + top_p=0.9, + presence_penalty=0.0 ) - - # Example call to work_stream - logging.info("Calling ai_agent.work_stream...") - # Extract generation parameters from config - temperature = config.get("content_temperature", 0.7) # Use a relevant temperature setting - top_p = config.get("content_top_p", 0.9) - presence_penalty = config.get("content_presence_penalty", 0.0) - - start_time = time.time() - stream_generator = ai_agent.work_stream( - system_prompt=system_prompt, - user_prompt=user_prompt, - info_directory=None, # No extra context folder for this test - temperature=temperature, - top_p=top_p, - presence_penalty=presence_penalty - ) - - # Process the stream - logging.info("Processing stream response:") - full_response = "" - for chunk in stream_generator: - print(chunk, end="", flush=True) # Keep print for stream output - full_response += chunk - - end_time = time.time() - logging.info(f"\n--- Stream Finished ---") - logging.info(f"Total time: {end_time - start_time:.2f} seconds") - logging.info(f"Total characters received: {len(full_response)}") - - except KeyError as e: - logging.error(f"Configuration error: Missing key '{e}'. Please check '{config_path}'.") + + # 异步迭代流 + async for content in async_stream: + # 累积完整响应 + full_response += content + # 实时打印内容 + print(content, end="", flush=True) + except Exception as e: - logging.exception("An error occurred during the stream test:") - finally: - # Ensure the agent is closed - if ai_agent: - logging.info("Closing AI Agent...") - ai_agent.close() - logging.info("AI Agent closed.") + print(f"\n生成过程中出错: {e}") + + end_time = time.time() + print(f"\n\n生成完成! 耗时: {end_time - start_time:.2f}秒") + + # 关闭agent + agent.close() + +async def main(): + """主函数""" + print("Testing AI_Agent streaming methods...") + + # 1. 测试同步流式响应 + demo_sync_stream() + + # 2. 测试回调式流式响应 + demo_callback_stream() + + # 3. 测试异步流式响应 + await demo_async_stream() + + print("\n所有测试完成!") if __name__ == "__main__": - main() \ No newline at end of file + # 运行异步主函数 + asyncio.run(main()) \ No newline at end of file