├── .DS_Store ├── patterns ├── .DS_Store ├── translate_en │ └── system.md ├── outliner_wsy │ └── system.md ├── translate_cn │ └── system.md ├── literature_review_exa │ └── system.md ├── write_wsy_tech_blog │ └── system.md ├── correct_cn_audio_transcription │ └── system.md ├── stylize_blog │ └── system.md ├── improved_trans_cn │ └── system.md ├── improved_trans_en │ └── system.md ├── comment_cn_trans │ └── system.md └── comment_en_trans │ └── system.md ├── env.example ├── .gitignore ├── config ├── correct_audio_transcription.yaml ├── correct_and_blog.yaml ├── literature_review_exa.yaml ├── translation_to_eng.yaml ├── translation_to_cn.yaml └── search_and_blog.yaml ├── memory ├── vocab.md ├── blog_example.md └── cn_paper_example.md ├── pyproject.toml ├── README.md ├── app_web.py └── app.py /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wshuyi/workflows_with_litellm_pub/HEAD/.DS_Store -------------------------------------------------------------------------------- /patterns/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wshuyi/workflows_with_litellm_pub/HEAD/patterns/.DS_Store -------------------------------------------------------------------------------- /env.example: -------------------------------------------------------------------------------- 1 | OPENROUTER_API_KEY = 2 | EXA_API_KEY = 3 | 4 | # OPENAI_API_KEY = 5 | # OPENAI_API_BASE = 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | logs 3 | archive 4 | replace_model_in_yaml.py 5 | zip_config_patterns.py 6 | addons.zip 7 | poetry.lock 8 | -------------------------------------------------------------------------------- /config/correct_audio_transcription.yaml: -------------------------------------------------------------------------------- 1 | strategies: 2 | - input_format: 'original text: 3 | 4 | {{text}} 5 | 6 | vocabularies: 7 | 8 | {{memory_vocab}} 9 | 10 | ' 11 | model: openrouter/anthropic/claude-3.5-sonnet 12 | output_name: correction 13 | prompt_name: correct_cn_audio_transcription 14 | -------------------------------------------------------------------------------- /memory/vocab.md: -------------------------------------------------------------------------------- 1 | # 常见词汇列表 2 | 3 | - Obsidian 4 | - 王树义 5 | - 玉树芝兰 6 | - Roam Research 7 | - Logseq 8 | - Keyboard Maestro 9 | - Typing Mind 10 | - Heptabase 11 | - 卡片笔记写作法 12 | - DALLE 13 | - Kimi Chat 14 | - 秘塔 AI 搜索 15 | - GLM-4 16 | - GPTs 17 | - Claude 18 | - 吕立青 19 | - SciSpace 20 | - Dify 21 | - Coze 22 | - Phidata 23 | - AutoGen 24 | - CrewAI 25 | - Streamlit 26 | - Perplexity 27 | - o1 Preview 28 | -------------------------------------------------------------------------------- /patterns/translate_en/system.md: -------------------------------------------------------------------------------- 1 | # IDENTITY and PURPOSE 2 | 3 | 您是一位专业的语言学家,专门从事其他语种到美式英语的直译工作。 4 | 5 | # OUTPUT INSTRUCTIONS 6 | 7 | - 自动识别给定文本的源语言。 8 | - 提供从识别的源语言到美式英语的专业翻译。 9 | - 保留所有的markdown图片链接。包括`![xxx](yyy)`和Obsidian风格的(`![[xxx]]`)。 10 | - 保留段落之间的空行。 11 | - 翻译要完整,忠实于原文 12 | - 只输出翻译内容,原文有什么你就翻译什么 13 | - 除翻译之外,不要提供任何解释或额外文本 14 | 15 | # OUTPUT FORMAT 16 | 17 | - 用美式英语输出翻译。 18 | - 不要包含任何评论、注释或解释。 19 | - 不要使用任何介绍性短语或结论。 20 | 21 | # INPUT 22 | 23 | INPUT: -------------------------------------------------------------------------------- /patterns/outliner_wsy/system.md: -------------------------------------------------------------------------------- 1 | 角色:资深编辑助理 2 | 3 | 目标:基于用户输入内容,生成逻辑组织后的完整文档。 4 | 5 | 任务说明: 6 | 1. 分析所有输入内容,生成三级层级大纲 7 | 2. 逐句检查原文,不要省略,分别一步步思考: 8 | - 阅读理解原文,尤其是text、highlights 9 | - 判断其归属的三级标题,并将内容输出在对应标题后 10 | - 对每一个输出的内容,都立即执行: 11 | - 输出对应的 text 或 highlights 原始文本,不要改动、提炼或翻译 12 | - 全部细节,尤其是数据、事例、图表、公式等,都要完整保留 13 | - 输出对应的 source url,标题,作者,发布时间信息 14 | 15 | 16 | 17 | 要求: 18 | - Markdown 格式 19 | - 原始材料成本很高,避免浪费任何内容 20 | - 请你输出完整的内容,不要有任何省略 21 | 22 | -------------------------------------------------------------------------------- /patterns/translate_cn/system.md: -------------------------------------------------------------------------------- 1 | # IDENTITY and PURPOSE 2 | 3 | 您是一位专业的语言学家,专门从事其他语种到简体中文(中国大陆使用)的直译工作。 4 | 5 | # OUTPUT INSTRUCTIONS 6 | 7 | - 自动识别给定文本的源语言。 8 | - 提供从识别的源语言到简体中文的专业翻译。 9 | - 保留所有的markdown图片链接。包括`![xxx](yyy)`和Obsidian风格的(`![[xxx]]`)。 10 | - 保留段落之间的空行。 11 | - 翻译要完整,忠实于原文 12 | - 只输出翻译内容,原文有什么你就翻译什么 13 | - 除翻译之外,不要提供任何解释或额外文本 14 | 15 | # OUTPUT FORMAT 16 | 17 | - 用简体中文输出翻译。 18 | - 不要包含任何评论、注释或解释。 19 | - 不要使用任何介绍性短语或结论。 20 | 21 | # INPUT 22 | 23 | INPUT: -------------------------------------------------------------------------------- /patterns/literature_review_exa/system.md: -------------------------------------------------------------------------------- 1 | # 角色设定 2 | 你是一位学术文献综述专家,擅长整合和分析研究资料。 3 | 4 | # 任务目标 5 | 撰写2000字学术文献综述,确保论证严谨,结构清晰。 6 | 7 | # 内容要求 8 | - 使用学术性简洁语言 9 | - 术语首次出现需附释义 10 | - 论点需有论证和论据支撑 11 | - 具体数据须用作论据 12 | - 如果数据不全,可以到搜索结果对应位置查找补充 13 | - 禁用感叹号 14 | 15 | # 格式规范 16 | - 仅使用到二级markdown标题 17 | - 采用中文自然段落 18 | - 对你的全部表述,都要引用原文,和对应的中文翻译 19 | - 引用采用inline markdown链接:`text [文献标题](URL)` 20 | - 源链接须对应内容位置 21 | 22 | 23 | # 禁止事项 24 | - 禁用数字引用格式[1] 25 | - 禁加额外标题 26 | - 禁造虚假来源 27 | - 禁用文末引用列表 28 | - 禁加提示性内容 -------------------------------------------------------------------------------- /patterns/write_wsy_tech_blog/system.md: -------------------------------------------------------------------------------- 1 | 角色定义: 2 | 3 | 你是一位善于用通俗语言讲解复杂技术概念的博主,写作风格清晰生动,喜欢与读者互动。 4 | 5 | 目标描述: 6 | 7 | 根据用户提供的全部资料与搜索结果全文,转换成一篇结构清晰完整、通俗易懂、富有个人风格的博客。 8 | 9 | 输出要求: 10 | 11 | • 请一步步思考 12 | • 用 Markdown 格式 13 | • 全文约5000字,详细表达内容。 14 | • 一级标题需吸引人,字数不超过20字。 15 | • 二、三级标题压缩至2-4字。 16 | • 有论点必须论证,并提供论据,包括来源。 17 | • 引用非中文原文时,翻译为中文,并注明来源。 18 | • 来源使用 Markdown 行内链接(abc xxx def),用原始资料的 URL 支持叙述。 19 | • 能用自然段表述的部分,一般不使用列表形式。 20 | • 除正文外,不输出任何提示性内容。 21 | • 不要输出无关内容或总结。 22 | • 样例参考 blog example。 23 | -------------------------------------------------------------------------------- /patterns/correct_cn_audio_transcription/system.md: -------------------------------------------------------------------------------- 1 | 角色:语音转写校对专家 2 | 3 | 目标:精准校正语音转写文本中的错误 4 | 5 | 背景:基于提供的词汇对照表和上下文逻辑 6 | 7 | 执行步骤: 8 | 1. 将文本分割为每段最多5句的段落 9 | 2. 按以下思路逐段分析: 10 | - 对照词汇列表检查专有名词 11 | - 结合上下文判断语义连贯性 12 | - 标识并修正错误 13 | 3. 除了修正错误,不要做任何改动 14 | 15 | 约束条件: 16 | 17 | - 输出Markdown格式 18 | - 只输出修改后内容,不要输出序号,也不需要输出改动的具体位置 19 | - 所有段落处理完毕之前,不要停下 20 | - 中文逗号用「,」,英文逗号用「,」。中文引号用 `「` 和 `」`,英文引号用「"」「"」 21 | 22 | 23 | 示例: 24 | 输入: 25 | "这是李白创作的静夜司。床前明月光,疑是地上双。" 26 | 27 | 输出: 28 | 29 | 这是李白创作的静夜思。床前明月光,疑是地上霜。 30 | 31 | -------------------------------------------------------------------------------- /config/correct_and_blog.yaml: -------------------------------------------------------------------------------- 1 | strategies: 2 | - input_format: 'original text: 3 | 4 | {{text}} 5 | 6 | vocabularies: 7 | 8 | {{memory_vocab}} 9 | 10 | ' 11 | model: openrouter/anthropic/claude-3.5-sonnet 12 | output_name: correction 13 | prompt_name: correct_cn_audio_transcription 14 | - input_format: 'original text: 15 | 16 | {{correction}} 17 | 18 | example: 19 | 20 | {{memory_blog_example}} 21 | 22 | ' 23 | model: openrouter/anthropic/claude-3.5-sonnet 24 | output_name: stylized 25 | prompt_name: stylize_blog 26 | -------------------------------------------------------------------------------- /patterns/stylize_blog/system.md: -------------------------------------------------------------------------------- 1 | 角色:文章改写专家,擅长调整文风 2 | 3 | 目标:将输入文稿,转化为指定表达方式 4 | 5 | 6 | 输出要求: 7 | 1. 语言风格 8 | - 使用第二人称「你」 9 | - 口语化表达 10 | - 保持专业性,增加亲和力 11 | 12 | 2. 内容处理 13 | - 一步步思考,逐段改写 14 | - 保留核心信息 15 | - 保留例子、数据等细节 16 | - 保持叙述逻辑的连贯性,避免跳跃 17 | 18 | 3. 应用具体技巧 19 | - 适度增加修辞手法 20 | - 适当使用比喻 21 | - 在关键处,设置引导性问题,注意给出答案 22 | - 鼓励读者实践 23 | 24 | 25 | 约束条件: 26 | 27 | - 输出Markdown格式 28 | - 所有段落处理完毕之前,不要停下 29 | - 仅输出改写后的内容,保持原文逻辑顺序 30 | - 内容前后不要添加任何内容(提示,总结) 31 | - 如无万分必要,禁止使用叹号 32 | 33 | 输出示例: 34 | 35 | 参考 blog_example 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "workflows_with_litellm" 3 | version = "0.1.1" 4 | description = "" 5 | authors = ["Shuyi Wang "] 6 | readme = "README.md" 7 | package-mode = false 8 | 9 | [tool.poetry.dependencies] 10 | python = "^3.11" 11 | pyperclip = "^1.9.0" 12 | litellm = "^1.51.0" 13 | langchain = "^0.2.7" 14 | python-dotenv = "^1.0.1" 15 | openai = "^1.35.14" 16 | generalagent = "^0.3.23" 17 | exa-py = "^1.0.16" 18 | schema = "^0.7.7" 19 | streamlit = "^1.38.0" 20 | 21 | 22 | [build-system] 23 | requires = ["poetry-core"] 24 | build-backend = "poetry.core.masonry.api" 25 | -------------------------------------------------------------------------------- /patterns/improved_trans_cn/system.md: -------------------------------------------------------------------------------- 1 | # IDENTITY and PURPOSE 2 | 3 | 您是一位久负盛名的大陆中文翻译家,擅长根据用户阅读习惯,在保持原意的情况下,把其他语种精细意译为简体中文(中国大陆使用)。 4 | 5 | # OUTPUT INSTRUCTIONS 6 | 7 | - 仔细阅读原文、直译结果以及翻译评论家提供的改进建议。 8 | - 根据给出的反馈和您的专业知识改进翻译。 9 | - 重点提高准确性、流畅性、风格和术语使用。 10 | - 保留所有的markdown图片链接。包括`![xxx](yyy)`和Obsidian风格的(`![[xxx]]`)。 11 | - 对于常用的技术术语,如"vertex"、"shader"、"z-buffer"等,在最终翻译结果保留原始英文术语。当技术术语首次出现时,提供其翻译,但在之后的所有实例中使用原始英文术语。 12 | - 注意我要求你**意译**,所以你要确保最终翻译文本符合中国大陆读者的阅读习惯 13 | 14 | 15 | # OUTPUT FORMAT 16 | 17 | - 只输出改进后的简体中文翻译。 18 | - 不要包含任何评论、注释或解释。 19 | - 不要使用任何介绍性短语或结论。 20 | 21 | 22 | # INPUT 23 | 24 | INPUT: 25 | -------------------------------------------------------------------------------- /patterns/improved_trans_en/system.md: -------------------------------------------------------------------------------- 1 | # IDENTITY and PURPOSE 2 | 3 | 您是一位久负盛名的美国英语翻译家,擅长根据用户阅读习惯,在保持原意的情况下,把其他语种精细意译为美式英语。 4 | 5 | # OUTPUT INSTRUCTIONS 6 | 7 | - 仔细阅读原文、直译结果以及翻译评论家提供的改进建议。 8 | - 根据给出的反馈和您的专业知识改进翻译。 9 | - 重点提高准确性、流畅性、风格和术语使用。 10 | - 保留所有的markdown图片链接。包括`![xxx](yyy)`和Obsidian风格的(`![[xxx]]`)。 11 | - 保留文内直接给出的链接 12 | - 对于常用的技术术语,如"vertex"、"shader"、"z-buffer"等,注意保持全文翻译的一致性 13 | - 注意我要求你**意译**,所以你要确保最终翻译文本符合美式英语读者的阅读习惯 14 | 15 | 16 | # OUTPUT FORMAT 17 | 18 | - 只输出改进后的美式英语翻译。 19 | - 不要包含任何评论、注释或解释。 20 | - 不要使用任何介绍性短语或结论。 21 | - 保持原先段落之间的空行分割 22 | - markdown 列表的换行原样保留 23 | 24 | 25 | 26 | # INPUT 27 | 28 | INPUT: 29 | -------------------------------------------------------------------------------- /config/literature_review_exa.yaml: -------------------------------------------------------------------------------- 1 | strategies: 2 | - input_format: '{{text}}' 3 | output_name: exa_result 4 | tool_name: exa_search 5 | tool_params: 6 | category: research paper 7 | - input_format: 'user input text: {{text}} search result: {{exa_result}}' 8 | model: openrouter/anthropic/claude-3-5-sonnet-20240620 9 | output_name: outlined_result 10 | prompt_name: outliner_wsy 11 | - input_format: 'outline filled: 12 | 13 | {{outlined_result}} 14 | 15 | search result: 16 | 17 | {{exa_result}} 18 | 19 | ' 20 | model: openrouter/anthropic/claude-3-5-sonnet 21 | output_name: literature_review_section 22 | prompt_name: literature_review_exa 23 | -------------------------------------------------------------------------------- /config/translation_to_eng.yaml: -------------------------------------------------------------------------------- 1 | strategies: 2 | - input_format: '{{text}}' 3 | model: openrouter/openai/gpt-4o-mini 4 | output_name: translation 5 | prompt_name: translate_en 6 | - input_format: 'original text: 7 | 8 | {{text}} 9 | 10 | translation: 11 | 12 | {{translation}} 13 | 14 | ' 15 | model: openrouter/anthropic/claude-3.5-sonnet 16 | output_name: reflection 17 | prompt_name: comment_en_trans 18 | - input_format: 'original text: 19 | 20 | {{text}} 21 | 22 | translation: 23 | 24 | {{translation}} 25 | 26 | comments: 27 | 28 | {{reflection}} 29 | 30 | ' 31 | model: openrouter/openai/gpt-4o-mini 32 | output_name: improvement 33 | prompt_name: improved_trans_en 34 | -------------------------------------------------------------------------------- /config/translation_to_cn.yaml: -------------------------------------------------------------------------------- 1 | strategies: 2 | - input_format: '{{text}}' 3 | model: openrouter/openai/gpt-4o-mini 4 | output_name: translation 5 | prompt_name: translate_cn 6 | - input_format: 'original text: 7 | 8 | {{text}} 9 | 10 | translation: 11 | 12 | {{translation}} 13 | 14 | ' 15 | model: openrouter/anthropic/claude-3.5-sonnet 16 | output_name: reflection 17 | prompt_name: comment_cn_trans 18 | - input_format: 'original text: 19 | 20 | {{text}} 21 | 22 | translation: 23 | 24 | {{translation}} 25 | 26 | comments: 27 | 28 | {{reflection}} 29 | 30 | ' 31 | model: openrouter/anthropic/claude-3.5-sonnet 32 | output_name: improvement 33 | prompt_name: improved_trans_cn 34 | -------------------------------------------------------------------------------- /patterns/comment_cn_trans/system.md: -------------------------------------------------------------------------------- 1 | # IDENTITY and PURPOSE 2 | 3 | 您是一位专业资深的翻译学评论家,专门做简体中文译本的翻译质量评估和改进翻译 4 | 5 | # OUTPUT INSTRUCTIONS 6 | 7 | - 用户的输入,包含了原文和直译后的文字 8 | - 一步步思考,从简体中文用户的阅读习惯出发,对 original text ,逐句话来处理 9 | - 对于每句话,如遇到需要修改的地方,使用以下格式: 10 | 原文: [以句子为单位,注意不需要输出整个儿句子,只需要输出该句子的前两个字符和后两个字符] 11 | 问题1:[当前句子中包含的某个问题] 12 | 修改建议1: [修改策略] 13 | 信心1: [0-100]% 14 | 问题2:[当前句子中包含的某个问题] 15 | 修改建议2: [修改策略] 16 | 信心2: [0-100]% 17 | 问题3:…… 18 | - 如果某一句话没有问题,输出「不需要修改」 19 | - 即便翻译没有错,但是译法不符合用户阅读习惯,也要忠实提出 20 | - 对于常用的技术术语,如"vertex"、"shader"、"z-buffer"等,在你的回答中保留原始的英文术语 21 | 22 | 23 | # OUTPUT FORMAT 24 | 25 | - 列出具体、有帮助和建设性的建议以改进翻译 26 | - 除了建议之外,不要包含任何评论、注释或解释。 27 | - 不要使用任何介绍性短语或结论。 28 | 29 | # INPUT 30 | 31 | INPUT: 32 | -------------------------------------------------------------------------------- /config/search_and_blog.yaml: -------------------------------------------------------------------------------- 1 | strategies: 2 | - input_format: '{{text}}' 3 | output_name: exa_news_result 4 | tool_name: exa_search 5 | tool_params: 6 | category: news 7 | - input_format: '{{text}}' 8 | output_name: exa_tweet_result 9 | tool_name: exa_search 10 | tool_params: 11 | category: tweet 12 | - input_format: 'user input text: {{text}} search result: {{exa_news_result}} {{exa_tweet_result}}' 13 | model: openrouter/anthropic/claude-3-5-sonnet-20240620 14 | output_name: outlined_result 15 | prompt_name: outliner_wsy 16 | - input_format: 'user input text: 17 | 18 | {{outlined_result}} 19 | 20 | blog example: {{memory_blog_example}}' 21 | model: openrouter/anthropic/claude-3-5-sonnet 22 | output_name: tech_blog_draft 23 | prompt_name: write_wsy_tech_blog 24 | -------------------------------------------------------------------------------- /patterns/comment_en_trans/system.md: -------------------------------------------------------------------------------- 1 | # IDENTITY and PURPOSE 2 | 3 | 您是一位美国专业资深的翻译学评论家,有20年编辑和文学评论经验。专门做美式英语译本的翻译质量评估和改进翻译 4 | 5 | # OUTPUT INSTRUCTIONS 6 | 7 | - 下面你全部的思考和输出,都要采用英文 8 | - 用户的输入,包含了原文和直译后的文字 9 | - 一步步思考,从美式英语用户的阅读习惯出发,对 original text ,逐句话来处理 10 | - 对于每句话,如遇到需要修改的地方,使用以下格式: 11 | 原文: [以句子为单位,注意不需要输出整个儿句子,只需要输出该句子的前两个字符和后两个字符] 12 | 问题1:[当前句子中包含的某个问题] 13 | 修改建议1: [修改策略] 14 | 信心1: [0-100]% 15 | 问题2:[当前句子中包含的某个问题] 16 | 修改建议2: [修改策略] 17 | 信心2: [0-100]% 18 | 问题3:…… 19 | - 如果某一句话没有问题,输出「不需要修改」 20 | - 即便翻译没有错,但是译法不符合用户阅读习惯,也要忠实提出 21 | - 除非特殊情况,否则结果里面不要保留任何中文。因为读者根本就看不懂 22 | - 专有名词如果本来已经是英文,保持原样不要动 23 | 24 | 25 | 26 | 27 | # OUTPUT FORMAT 28 | 29 | - 用英文列出具体、有帮助和建设性的建议以改进翻译 30 | - 除了建议之外,不要包含任何评论、注释或解释。 31 | - 不要使用任何介绍性短语或结论。 32 | 33 | # INPUT 34 | 35 | INPUT: 36 | -------------------------------------------------------------------------------- /memory/blog_example.md: -------------------------------------------------------------------------------- 1 | # style 样例 2 | 3 | 以下是参考的 style 样例。参考其语言风格与格式,而不要参考其具体内容。 4 | 5 | 6 | 7 | 之前吴恩达老师在 deeplearning.ai 平台上已经与各种机构 —— 例如 LangChain,OpenAI 等等 —— 合作开发了一系列短课程,我都已经推荐给了学生和读者。 8 | 9 | 尽管这些课程讲的内容非常新颖,内容也很有价值,但多是对方主讲。其讲授方法和吴恩达教授比起来,差得太远了。 10 | 11 | 吴恩达老师的讲课方式,我想用两个词来总结——清晰和生动。 12 | 13 | 先说清晰。 14 | 15 | 下面是视频里出现的一张幻灯,讲大语言模型训练中自监督学习阶段的例子。 16 | 17 | 以前我在上课或讲座时经常提到「自监督学习」,并尝试举例来说明 LLM 是如何被训练「预测下一个词儿」的。但一对比就会发现,只需要拿出这样一句话的例子,逐步迭代说明 Input (A) 和 Output (B) 的对应变化,观众立即就能弄懂「自监督」的概念了。比起大段语言描述,这种例证的方式要直观和清晰许多。 18 | 19 | 再比如第一周的末尾,吴恩达老师讲了 Stable Diffusion 生成式绘图的原理。 20 | 21 | 我之前从各种教程上也都看过类似的讲解。各位主讲老师介绍方式也很生动,包括这些图片不断加入噪声的过程,有的还做成了动画。只不过,这里同样就是加上了个 Input(A) 和 Output(B) ,对照着上面的图片,不断渐进出现,你立刻就能明白同样的模型如何对每一步,都能逐步推测更为清晰图像的过程。在我看来,能够用最简短的语言和直接的方式传递知识,就叫做「清晰」。 22 | 23 | 下面聊聊「生动」。吴恩达老师讲解的方式风趣幽默,用例非常吸引人。 24 | 25 | 例如提到大语言模型的「幻觉」(Hallucinations),吴恩达老师提到 LLM 如何编造答案来取悦用户。 26 | 27 | 这里的例子就很有趣,他问莎士比亚的哪些诗提到了碧昂丝(Beyonce)。这简直就是西方版的「关公战秦琼」嘛,可是 LLM 真的就敢「顺杆儿爬」给你编造啊。 28 | 29 | 当然,幻觉带来的后果绝不仅仅是好玩儿,下面这个就很严重。 30 | 31 | 课件中提到,一个律师使用 ChatGPT 提供的答案,直接提交给法庭。后果如何?课程里面有,自己看吧。 32 | 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Readme 2 | 3 | ## English version 4 | 5 | Run workflow easily with: 6 | 7 | 1. YAML file 8 | 2. openrouter.ai APIs 9 | 3. fabric-like patterns 10 | 11 | For details, please look into [this article](https://wshuyi.medium.com/how-to-create-and-run-your-own-ai-workflows-with-ease-1a611e16d48f) for the basic operation and [this one](https://www.patreon.com/posts/how-to-use-line-110869551?utm_medium=clipboard_copy&utm_source=copyLink&utm_campaign=postshare_creator&utm_content=join_link) for advanced academic use. 12 | 13 | ## 中文说明(Chinese version) 14 | 15 | 有了这个简单的 Python 项目,你使用 AI 工作流,就不需要跟 fabric 的各种安装配置打交道了。 16 | 17 | 这个项目包含: 18 | - 可以用 YAML 文件配置的 AI 工作流 19 | - 在各个节点上,可以分别指定不同的 openrouter.ai API 调用大语言模型 20 | - 所有 patterns 包含的结构化提示词,参考 fabric 写法,大多可以在两个项目之间兼容复用 21 | 22 | 基础操作请参考[这篇文章](https://mp.weixin.qq.com/s/tHIbczDuVfNdbmJlN6Xs9A),进阶学术用途请参考[这篇文章](https://mp.weixin.qq.com/s/Xbfhh0w6-iNBpE7FExrRrA) 23 | 24 | 我为你录制了安装配置的视频教程,请[点击这个链接](https://www.bilibili.com/video/BV1Gs1GY5EUE/)。 25 | 26 | 注意我录制的视频教程是以 macOS 系统为例,后来又让学生帮我录制了 Windows 系统的安装过程演示,请[点击这个链接](https://www.bilibili.com/video/BV18vS8YNEdw/)查看。 -------------------------------------------------------------------------------- /app_web.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | import subprocess 3 | import tempfile 4 | import os 5 | from dotenv import load_dotenv 6 | import yaml 7 | 8 | # Load environment variables 9 | load_dotenv() 10 | 11 | # Get the directory of the current script 12 | current_dir = os.path.dirname(os.path.abspath(__file__)) 13 | 14 | def create_api_key_input(key_name, env_var_name): 15 | """创建API key输入框并处理其逻辑""" 16 | # 直接从.env文件读取值 17 | env_path = os.path.join(os.path.dirname(__file__), '.env') 18 | env_value = "" 19 | if os.path.exists(env_path): 20 | with open(env_path, 'r') as f: 21 | for line in f: 22 | if line.strip().startswith(f"{env_var_name} ="): 23 | env_value = line.split('=')[1].strip() 24 | break 25 | 26 | # 创建密码输入框 27 | api_key = st.text_input( 28 | f"{key_name} API Key", 29 | value=env_value, 30 | type="password", 31 | help=f"Enter your {key_name} API key" 32 | ) 33 | 34 | # 如果用户输入了新的API key且与环境变量不同 35 | if api_key and api_key != env_value: 36 | if os.path.exists(env_path): 37 | with open(env_path, 'r') as f: 38 | lines = f.readlines() 39 | 40 | # 查找并更新API key 41 | key_found = False 42 | for i, line in enumerate(lines): 43 | if line.strip().startswith(f"{env_var_name} ="): 44 | lines[i] = f"{env_var_name} = {api_key}\n" 45 | key_found = True 46 | break 47 | 48 | if not key_found: 49 | lines.append(f"{env_var_name} = {api_key}\n") 50 | 51 | # 写入更新后的内容 52 | with open(env_path, 'w') as f: 53 | f.writelines(lines) 54 | else: 55 | # 如果.env文件不存在,创建新文件 56 | with open(env_path, 'w') as f: 57 | f.write(f"{env_var_name} = {api_key}\n") 58 | 59 | # 更新环境变量 60 | os.environ[env_var_name] = api_key 61 | 62 | return api_key 63 | 64 | def load_workflows(): 65 | workflows = [] 66 | config_dir = os.path.join(current_dir, 'config') 67 | for file in os.listdir(config_dir): 68 | if file.endswith('.yaml'): 69 | workflows.append(file.replace('.yaml', '')) 70 | return workflows 71 | 72 | def load_config(workflow): 73 | config_path = os.path.join(current_dir, 'config', f'{workflow}.yaml') 74 | with open(config_path, 'r', encoding='utf-8') as f: 75 | return yaml.safe_load(f) 76 | 77 | # Streamlit app 78 | st.title('Text Processing Workflow') 79 | 80 | # API Keys section in sidebar 81 | with st.sidebar: 82 | st.header('API Keys') 83 | openrouter_api_key = create_api_key_input("OpenRouter", "OPENROUTER_API_KEY") 84 | exa_api_key = create_api_key_input("EXA", "EXA_API_KEY") 85 | 86 | # 添加OpenAI设置 87 | st.subheader('OpenAI Settings') 88 | openai_api_key = create_api_key_input("OpenAI", "OPENAI_API_KEY") 89 | 90 | # 从.env文件读取 OPENAI_API_BASE 91 | env_path = os.path.join(os.path.dirname(__file__), '.env') 92 | env_base_url = "" 93 | if os.path.exists(env_path): 94 | with open(env_path, 'r') as f: 95 | for line in f: 96 | if line.strip().startswith('OPENAI_API_BASE ='): 97 | env_base_url = line.split('=')[1].strip() 98 | break 99 | 100 | openai_api_base = st.text_input( 101 | "OpenAI API Base URL", 102 | value=env_base_url, 103 | help="Enter your OpenAI API base URL (optional)" 104 | ) 105 | 106 | # 如果用户输入了新的API base且与环境变量不同 107 | if openai_api_base and openai_api_base != env_base_url: 108 | if os.path.exists(env_path): 109 | with open(env_path, 'r') as f: 110 | lines = f.readlines() 111 | 112 | base_found = False 113 | for i, line in enumerate(lines): 114 | if line.strip().startswith("OPENAI_API_BASE ="): 115 | lines[i] = f"OPENAI_API_BASE = {openai_api_base}\n" 116 | base_found = True 117 | break 118 | 119 | if not base_found: 120 | lines.append(f"OPENAI_API_BASE = {openai_api_base}\n") 121 | 122 | with open(env_path, 'w') as f: 123 | f.writelines(lines) 124 | else: 125 | with open(env_path, 'w') as f: 126 | f.write(f"OPENAI_API_BASE = {openai_api_base}\n") 127 | 128 | os.environ["OPENAI_API_BASE"] = openai_api_base 129 | 130 | # Workflow selection 131 | workflows = load_workflows() 132 | selected_workflow = st.selectbox('Select Workflow', workflows) 133 | 134 | # Load config for selected workflow 135 | config = load_config(selected_workflow) 136 | 137 | # Text input 138 | input_text = st.text_area('Enter text to process') 139 | 140 | # Process button 141 | if st.button('Process'): 142 | if input_text: 143 | # Save input text to a temporary file 144 | with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt', encoding='utf-8') as temp_file: 145 | temp_file.write(input_text) 146 | temp_file_path = temp_file.name 147 | 148 | # Prepare command with full path to app.py 149 | app_path = os.path.join(current_dir, 'app.py') 150 | cmd = ['poetry', '--directory', current_dir, 'run', 'python', app_path, temp_file_path, '--workflow', selected_workflow] 151 | 152 | # Run the command 153 | try: 154 | result = subprocess.run(cmd, capture_output=True, text=True, check=True) 155 | 156 | # Display output and provide download link 157 | output_file = os.path.join(os.path.dirname(temp_file_path), f'{selected_workflow}-output.md') 158 | if os.path.exists(output_file): 159 | with open(output_file, 'r', encoding='utf-8') as f: 160 | output_content = f.read() 161 | st.text_area('Output', output_content, height=300) 162 | st.download_button('Download Output', output_content, file_name=f'{selected_workflow}-output.md') 163 | else: 164 | st.warning('Output file not found. Displaying standard output and error for debugging:') 165 | st.text_area('Standard Output', result.stdout, height=300) 166 | if result.stderr: 167 | st.text_area('Standard Error', result.stderr, height=300) 168 | 169 | except subprocess.CalledProcessError as e: 170 | st.error(f'Error occurred. Displaying standard output and error for debugging:') 171 | st.text_area('Standard Output', e.stdout, height=300) 172 | st.text_area('Standard Error', e.stderr, height=300) 173 | 174 | finally: 175 | # Clean up temporary file 176 | os.unlink(temp_file_path) 177 | else: 178 | st.warning('Please enter some text to process.') 179 | -------------------------------------------------------------------------------- /memory/cn_paper_example.md: -------------------------------------------------------------------------------- 1 | 以下是中文论文的示例,供参考: 2 | 3 | ## 引言 4 | 5 | 2022 年 11 月 30 日,OpenAI 发布了名为 ChatGPT 的模型研究预览版,它可以用对话的方式与用户进行交互。ChatGPT 模型使用人类反馈的强化学习(Reinforcement Learning from Human Feedback, RLHF)进行训练 [@ouyangTrainingLanguageModels2022]。训练方法与 OpenAI 早前发布的 InstructGPT 类似,但数据收集设置略有不同。 OpenAI 使用有监督的微调方法,基于 GPT-3.5 系列的模型训练了一个初始模型,并且用人工 AI 训练师对话形式,混合 InstructGPT 数据集撰写对话格式的回应。对于备选答案,由人工 AI 训练师排名提供增强学习的奖励 [@ChatGPTOptimizingLanguage2022]。 6 | 7 | ChatGPT 自发布以来变得非常受欢迎,据报道仅在前五天就吸引了超过 100 万用户 [@jacksonOpenAIExecutivesSay],在上市的第一个月中则拥有了 5700 万活跃用户 [@ChatGPTUserBase2023]。据估计,仅在发布后两个月内,其月活跃用户就达到了 1 亿 [@ChatGPTExplosivePopularity]。ChatGPT 的广泛普及使得 OpenAI 的价值增长到了 290 亿美元 [@southernChatGPTPopularityBoosts2023]��� 8 | 9 | ChatGPT 的火爆,伴随着一系列对它的讨论。人们津津乐道于它通过了图灵测试 [@yalalovChatGPTPassesTuring2022],在明尼苏达大学通过了法律考试,并在加州大学伯克利分校的另一场考试中获得了优异成绩 [@kellyChatGPTPassesExams2023]。人们尝试用它写简历和求职信,解释复杂的话题,提供恋爱建议 [@timothy11ThingsYou2022]。广泛的试用中,用户们逐渐发现了 ChatGPT 的许多问题,例如对话容量限制,成为抄袭和作弊利器,偏见和歧视以及准确性等问题 [@BiggestProblemsChatGPT2023]。 10 | 11 | 尽管大众对 ChatGPT 的讨论已经非常激烈,而且丰富多彩,作为科研人员,我们似乎更应该严肃审视 ChatGPT 以及相似模型和服务的出现,会给学术界带来什么样的变化。在变化出现的时候,该如何抓住机遇,同时避免负面影响,从而获得科研竞争优势?本文通过实际的例证,来尝试初步回答这一问题。 12 | 13 | ## 文献回顾 14 | 15 | NLG(Neural Language Generation,自然语言生成)是从非语言表示生成人类可以理解的文本的技术,应用广泛,包括机器翻译、对话系统、文本摘要等等 [@dongSurveyNaturalLanguage2023]。目前主要的 NLG 模型包括:Transformer、GPT-1/2/3、BERT、XLM、BART、Codex 等。其中,Transformer 模型基于 Attention 机制,比之前的模型在质量和用时上都有所提升 [@vaswaniAttentionAllYou2017];GPT 模型为使用大量数据训练好的基于 Transformer 结构的生成型预训练变换器模型,能在常识推理、问题问答、语义蕴含的语言场景中取得改进 [@radfordImprovingLanguageUnderstanding];BERT 引入了 MLM 和 NSP 训练方法,可以融合上下文 [@wangSpectrumBERTPretrainingDeep2022];XLM 模型通过训练跨语言信息,可以用在训练语料少的语言上学习到的信息 [@lampleCrosslingualLanguageModel2019]。2020 年 OpenAI 发布的 GPT-3 模型参数达到 1750 亿个,通过与模型的文本互动来指定任务,性能强大 [@brownLanguageModelsAre2020];2021 年,OpenAI 又发布了基于 GPT-3 的 Codex 模型,可以从自然语言文件串中产生功能正确的代码 [@chenEvaluatingLargeLanguage2021]。2022 年,OpenAI 发布了基于 GPT-3 的 InstructGPT 模型,加入了人类评价及反馈数据,可以遵循人类指令,并可以泛化到没有见过的任务中 [@ouyangTrainingLanguageModels2022];ChatGPT 是 InstructGPT 模型的兄弟模型,能以遵循提示中的指令并提供详细的响应,回答遵循人类价值观 [@ChatGPTOptimizingLanguage2022]。 16 | 17 | AIGC(AI Generated Content)是指利用人工智能技术来生��内容的技术,包括文本到文本的语言模型、文本到图像的生成模型、从图像生成文本等等。其中,谷歌发布的 LaMDA 是基于 Transformer 的用于对话的语言模型,利用外部知识源进行对话,达到接近人类水平的对话质量 [@thoppilanLaMDALanguageModels2022];Meta AI 推出的 PEER 是可以模拟人类写作过程的文本生成模型 [@schickPEERCollaborativeLanguage2022];OpenAI 发布的 Codex 和 DeepMind 的 AlphaCode 是分别用于从文本生成代码的生成模型 [@chenEvaluatingLargeLanguage2021;@liCompetitionlevelCodeGeneration2022]。在图像生成方面,GauGAN2 和 DALL・E 2 分别是可以生成风景图像和从自然语言的描述生成现实图像的生成模型,基于 GAN 和 CLIP 模型,使用对比学习训练 [@salianNVIDIAResearchGauGAN2021;@rameshHierarchicalTextConditionalImage2022];Midjourney 和 Stable Diffusion 是从文本到图像的 AI 生成模型,而谷歌的 Muse 则实现了最先进的文本转换为图像的生成性能 [@rombachHighResolutionImageSynthesis2022]。另外,Flamingo 是一个视觉语言模型,能将图像、视频和文本作为提示输出相关语言 [@alayracFlamingoVisualLanguage];VisualGPT 是 OpenAI 推出的从图像到文本的生成模型 [@chenVisualGPTDataefficientAdaptation2022]。 18 | 19 | 人工智能内容产生过程中,难以避免遇到各种问题。例如偏见和歧视问题。由于训练数据集可能存在偏见和歧视,ChatGPT 可能会学习到这些偏见或歧视,因此需要采用多种方法对数据进行筛选和清洗,或者使用公平性算法来纠正模型偏差。总体而言,ChatGPT 的公平性取决于它的训练数据集以及使用它时的上下文和提问方式 [@brownLanguageModelsAre2020]。另外还有算力的挑战。ChatGPT 依赖着大量算力来训练海量文本数据,以此学习语言模式和知识。算力需求日益增长,致使这一领域存在着技术垄断,会对算力的进一步提升、大数据的训练等进一步行动产生影响。OpenAI 为了应对 ChatGPT 的高需求,已经采取了排队系统和流量整形等措施 [@morrisonComputePowerBecoming2022]。 20 | 21 | 根据本研究对相关成果的梳理,尚未发现详细分析与论述ChatGPT对科研工作者影响的研究论文。因此,本文准备从ChatGPT给科研工作者带来的机遇与挑战这两个方面作为切入点,展开论述。 22 | 23 | ## 机遇 24 | 25 | ChatGPT 是一种 AI 工程化的成功实践。AI 工程化专注于开发工具、系统和流程,使得人工智能能够应用于现实世界的领域[@WhatAIEngineer2022]。它使得普通人可以应用最新的自然语言生成与对话技术���完成很多曾经需要技术门槛才能完成的工作。 26 | 27 | ### 辅助编程 28 | 29 | 数据科学的研究范式已经深刻影响了许多学科。许多研究都需要通过不同形式来掌握足够的数据支持。通常,研究数据获取的途径主要有三种:开放数据集、API 调用和爬虫(Web Scrapper)。Python 语言是信息管理学科中进行数据分析最常用的编程语言。以前,用户必须掌握 Python 语言的基础语法,了解 Python 环境的使用,才能编写程序进行 API 调用或利用爬虫搜集数据。但现在有了 ChatGPT,用户可以通过自然语言对话的形式,给出要求,人工智能会直接给出源代码,并复制到实验环境中,从而获取所需数据。 30 | 31 | @fig-通过浏览器定位爬取范围 演示了研究者打算爬取某个网页上的信息,于是可以通过浏览器的 Inspector 功能查找对应的区域,此处找到的是类别 `sdyw_ul` 。 32 | 33 | ![通过浏览器定位爬取范围](assets/c92182ecda2fa2cc07d9efa8d766e22a50c4e73e.png){#fig-通过浏览器定位爬取范围} 34 | 35 | 有了对应的爬取位置,用户就可以在 ChatGPT 里面直接提出要求:"我想获得 sdyw_ul 类下的链接和标题。"(I want to get the links and titles under the class "sdyw_ul")。然后,ChatGPT 自动编写程序,结果如 @fig-chatgpt自动编程爬虫 所示。 36 | 37 | ![ChatGPT 自动编程爬虫](assets/415dce89d5c8cf640fc2cda22e790af377419d47.png){#fig-chatgpt自动编程爬虫} 38 | 39 | 如果用户对程序运行结果不满意,可以通过进一步对话交流,让 ChatGPT 对程序源代码进行修改。例如可以通过这样的对话,让 ChatGPT 把数据输出的方式改成 CSV 文件。ChatGPT更新代码,返回结果如 @fig-chatgpt爬虫编程修改输出格式 所示。 40 | 41 | ![ChatGPT 爬虫编程修改输出格式](assets/59daadf49bea15f072504a5b2ac4e8cb7870123a.png){#fig-chatgpt爬虫编程修改输出格式} 42 | 43 | 因为 ChatGPT 对多轮对话的记忆力,所以每次只需要提出进一步的要求,即可不断让 ChatGPT 编写符合用户目标的程序,最终完成预期目标。 44 | 45 | 46 | 47 | ![ChatGPT 生成爬虫的最终运行结果](assets/38d8d26310dc4e87c963b21bf040a127acb49f1f.png){#fig-chatgpt生成爬虫的最终运行结果} 48 | 49 | 最终,用户可以仅通过自然语言交互和拷贝 ChatGPT 生成结果代码并运行的方式,把该网站上全部感兴趣的内容,存入到 Excel 文件中,如 @fig-chatgpt生成爬虫的最终运行结果 所示。 50 | 51 | ChatGPT 辅助编程,还不止体现在数据采集环节。ChatGPT 的基础模型是 "GPT-3.5",底层基础模型是在大量代码上训练的结果,称为 code-davinci-002 [@OpenAIAPI]。因此,ChatGPT 见识过大量产生于 2021 年第四季度之前的代码段,并且对代码上下文补全有较强的能力。在此之前的数据分析甚至是机器学习模型训练工作,都可以通过自然语言对话的方式,交给 ChatGPT 来进行。 52 | 53 | 例如下面的例子里,用户尝试让 ChatGPT 采用 LDA 对一系列英文新闻文本做出主题挖掘。提出的自然语言指令和ChatGPT 的应答如 @fig-chatgpt编写lda主题挖掘代码 所示。 54 | 55 | ![ChatGPT 编写 LDA 主题挖掘代码](assets/7c79163f4badebaa5a8b44147ccfaa6de5e8fa97.png){#fig-chatgpt编写lda主题挖掘代码} 56 | 57 | 用户只需将ChatGPT给出的代码复制运行,对应生成的 LDA 主题聚类可视化结果如 @fig-chatgpt辅助编程生成lda主题聚类可视化结果 所示。 58 | 59 | ![ChatGPT 辅助编程生成 LDA 主题聚类可视化结果](assets/6013fb547f5cc75cd4e8848de93facef89e772f0.png){#fig-chatgpt辅助编程生成lda主题聚类可视化结果} 60 | 61 | 如图可见,原本需要一定的编程基础才能完成的机器学习乃至数据可视化任务,都可以通过和 ChatGPT 自然语言对话方式来达成。 62 | 63 | 而如果用户觉得结果有不满意的地方,可以随时跟 ChatGPT 交互,从而做出订正。例如原本的代码中,ChatGPT 默认为我们采用中文停用词,并且还使用 jieba 软件包进行分词。我们可以要求 ChatGPT 改用英语停用词进行处理。ChatGPT会立即根据新的要求变动,给出更新后的代码,如 @fig-要求chatgpt改用英文停用词表 所示。 64 | 65 | ![要求 ChatGPT 改用英文停用词表](assets/5379d1ab3082a281d5586bff44fd1afa8230959e.png){#fig-要求chatgpt改用英文停用词表} 66 | 67 | 在这个例子中,ChatGPT 改用了 nltk 软件包,使用内置的英文停用词表,可以做出更加符合要求的结果。 68 | 69 | 不仅如此,在大部分 ChatGPT 生成的代码中,不仅会有详细的注释。代码完成后,ChatGPT 还会给出 @fig-chatgpt给出的代码附加解释 这样相应的解释。 70 | 71 | ![ChatGPT 给出的代码附加解释](assets/d32689e146cbe33b97be3908468dca43bd232b5b.png){#fig-chatgpt给出的代码附加解释} 72 | 73 | 这对于我们了解代码实际的功用,并且在其上进行修改迭代甚至是查找错误,都非常有帮助。对于想学习编程的入门级研究人员,也会起到显著的帮助作用。 74 | 75 | ### 辅助阅读 76 | 77 | 做研究,免不了要阅读文献。在信息管理学科,期刊数量众多,而且外文期刊所占比例很大,每年都涌现出很多的新文章需要阅读。及时对文章重点进行把握,有利于在科研进度上保持领先。但是众多的文献阅读、消化、理解,尤其是外文文献的阅读,也对本领域研究者的构成了较大的挑战。 78 | 79 | 有了 ChatGPT,研究者可以将外文论文中的内容直接输入进来,然后利用提问的形式,要求 ChatGPT 自动提炼重点内容。 80 | 81 | 我们就以描述 ChatGPT 同类模型 InstrctGPT 的论文 "Training language models to follow instructions with human feedback" [@ouyangTrainingLanguageModels2022] 中的 3.5 节 "Models" 为例,输入其中主体部分到 ChatGPT ,给出的提示词是 "请用中文从下面资料中提炼出至少三个重点"。输入内容如 @fig-chatgpt自动提炼重点输入部分 所示。 82 | 83 | ![ChatGPT 自动提炼重点输入部分](assets/8cc438293eefe605a65a13583449118087ffef6c.png){#fig-chatgpt自动提炼重点输入部分} 84 | 85 | @fig-chatgpt自动提炼重点输出部分 是 ChatGPT 给出的答案。可见ChatGPT可以正确理解用户的要求,并且对内容进行了正确的自动总结。 86 | 87 | ![ChatGPT 自动提炼重点输出部分](assets/a74fb60664043b908b7485719ade4f1be2a2f30e.png){#fig-chatgpt自动提炼重点输出部分} 88 | 89 | 在模型学习训练集材料中,已经接触过不少专有名词,所以我们甚至可以不进行任何输入,直接让 ChatGPT 帮助解释一些专有名词。例如 @fig-chatgpt自动提炼重点输出部分 里答案中出现了 "深度强化学习",我们可以让 ChatGPT 尝试讲解其含义。输入的提示词为 "什么是'深度强化学习',在上述文稿里面的作用是什么?"。 @fig-解释深度强化学习 是 ChatGPT 给出的回答: 90 | 91 | ![解释深度强化学习](assets/cde71f85f2097fb2114f0f3dc5dfc1c833335e2a.png){#fig-解释深度强化学习} 92 | 93 | 我们可以对 @fig-解释深度强化学习 中出现的新专有名词继续提问,例如 "赌徒困境" 是什么?ChatGPT的回答如 @fig-chatgpt解释赌徒困境 所示。 94 | 95 | ![ChatGPT 解释赌徒困境](assets/a2ecd34240dce59307e24975b737bfe5818d4adc.png){#fig-chatgpt解释赌徒困境} 96 | 97 | 如果对 ChatGPT 总结的内容不放心,用户还可以让 ChatGPT 找到与专有名词对应的原文文本。 @fig-查找专有名词对应的原始文本 为ChatGPT自动找出的"赌徒困境"原始文本。 98 | 99 | ![查找专有名词对应的原始文本](assets/914e12bc7af6f7a3f5d8fc8e58133528201810af.png){#fig-查找专有名词对应的原始文本} 100 | 101 | 通过 ChatGPT 展示原文的文本,研究者可以加以印证,证明 ChatGPT 总结没有偏离原文的叙述。 102 | 103 | 另外,用户还可以对文本提出问题,ChatGPT 会尽全力尝试解答。例如示例论文这样的讲述模型训练方法的作品,研究者可能更感兴趣一种模型获取反馈与提升改进的流程,并且用它和其他同类模型进行比对。所以我们可以问出一个非常综合性的问题 "模型是如何获得反馈和改进,达到训练目标的?" @fig-chatgpt对阅读材料的综合性问题解答 是 ChatGPT 的回答。 104 | 105 | ![ChatGPT 对阅读材料的综合性问题解答](assets/02692ffe0470f7e6a8eca0694ea4c5d9e20ace0d.png){#fig-chatgpt对阅读材料的综合性问题解答} 106 | 107 | 可以看到,ChatGPT 对文本语义理解非常准确,而且还用中文进行了流畅自然的翻译。特别的,对于文中出现的专有名词,例如 "SFT" 等,都用英文全程和缩写加以注明。 108 | 109 | 以下是中文论文的示例,供参考: 110 | 111 | ## 引言 112 | 113 | 2022 年 11 月 30 日,OpenAI 发布了名为 ChatGPT 的模型研究预览版,它可以用对话的方式与用户进行交互。ChatGPT 模型使用人类反馈的强化学习(Reinforcement Learning from Human Feedback, RLHF)进行训练 [@ouyangTrainingLanguageModels2022]。训练方法与 OpenAI 早前发布的 InstructGPT 类似,但数据收集设置略有不同。 OpenAI 使用有监督的微调方法,基于 GPT-3.5 系列的模型训练了一个初始模型,并且用人工 AI 训练师对话形式,混合 InstructGPT 数据集撰写对话格式的回应。对于备选答案,由人工 AI 训练师排名提供增强学习的奖励 [@ChatGPTOptimizingLanguage2022]。 114 | 115 | ChatGPT 自发布以来变得非常受欢迎,据报道仅在前五天就吸引了超过 100 万用户 [@jacksonOpenAIExecutivesSay],在上市的第一个月中则拥有了 5700 万活跃用户 [@ChatGPTUserBase2023]。据估计,仅在发布后两个月内,其月活跃用户就达到了 1 亿 [@ChatGPTExplosivePopularity]。ChatGPT 的广泛普及使得 OpenAI 的价值增长到了 290 亿美元 [@southernChatGPTPopularityBoosts2023]。 116 | 117 | ChatGPT 的火爆,伴随着一系列对它的讨论。人们津津乐道于它通过了图灵测试 [@yalalovChatGPTPassesTuring2022],在明尼苏达大学通过了法律考试,并在加州大学伯克利分校的另一场考试中获得了优异成绩 [@kellyChatGPTPassesExams2023]。人们尝试用它写简历和求职信,解释复杂的话题,提供恋爱建议 [@timothy11ThingsYou2022]。广泛的试用中,用户们逐渐发现了 ChatGPT 的许多问题,例如对话容量限制,成为抄袭和作弊利器,偏见和歧视以及准确性等问题 [@BiggestProblemsChatGPT2023]。 118 | 119 | 尽管大众对 ChatGPT 的讨论已经非常激烈,而且丰富多彩,作为科研人员,我们似乎更应该严肃审视 ChatGPT 以及相似模型和服务的出现,会给学术界带来什么样的变化。在变化出现的时候,该如何抓住机遇,同时避免负面影响,从而获得科研竞争优势?本文通过实际的例证,来尝试初步回答这一问题。 120 | 121 | ## 文献回顾 122 | 123 | NLG(Neural Language Generation,自然语言生成)是从非语言表示生成人类可以理解的文本的技术,应用广泛,包括机器翻译、对话系统、文本摘要等等 [@dongSurveyNaturalLanguage2023]。目前主要的 NLG 模型包括:Transformer、GPT-1/2/3、BERT、XLM、BART、Codex 等。其中,Transformer 模型基于 Attention 机制,比之前的模型在质量和用时上都有所提升 [@vaswaniAttentionAllYou2017];GPT 模型为使用大量数据训练好的基于 Transformer 结构的生成型预训练变换器模型,能在常识推理、问题问答、语义蕴含的语言场景中取得改进 [@radfordImprovingLanguageUnderstanding];BERT 引入了 MLM 和 NSP 训练方法,可以融合上下文 [@wangSpectrumBERTPretrainingDeep2022];XLM 模型通过训练跨语言信息,可以用在训练语料少的语言上学习到的信息 [@lampleCrosslingualLanguageModel2019]。2020 年 OpenAI 发布的 GPT-3 模型参数达到 1750 亿个,通过与模型的文本互动来指定任务,性能强大 [@brownLanguageModelsAre2020];2021 年,OpenAI 又发布了基于 GPT-3 的 Codex 模型,可以从自然语言文件串中产生功能正确的代码 [@chenEvaluatingLargeLanguage2021]。2022 年,OpenAI 发布了基于 GPT-3 的 InstructGPT 模型,加入了人类评价及反馈数据,可以遵循人类指令,并可以泛化到没有见过的任务中 [@ouyangTrainingLanguageModels2022];ChatGPT 是 InstructGPT 模型的兄弟模型,能以遵循提示中的指令并提供详细的响应,回答遵循人类价值观 [@ChatGPTOptimizingLanguage2022]。 124 | 125 | AIGC(AI Generated Content)是指利用人工智能技术来生成内容的技术,包括文本到文本的语言模型、文本到图像的生成模型、从图像生成文本等等。其中,谷歌发布的 LaMDA 是基于 Transformer 的用于对话的语言模型,利用外部知识源进行对话,达到接近人类水平的对话质量 [@thoppilanLaMDALanguageModels2022];Meta AI 推出的 PEER 是可以模拟人类写作过程的文本生成模型 [@schickPEERCollaborativeLanguage2022];OpenAI 发布的 Codex 和 DeepMind 的 AlphaCode 是分别用于从文本生成代码的生成模型 [@chenEvaluatingLargeLanguage2021;@liCompetitionlevelCodeGeneration2022]。在图像生成方面,GauGAN2 和 DALL・E 2 分别是可以生成风景图像和从自然语言的描述生成现实图像的生成模型,基于 GAN 和 CLIP 模型,使用对比学习训练 [@salianNVIDIAResearchGauGAN2021;@rameshHierarchicalTextConditionalImage2022];Midjourney 和 Stable Diffusion 是从文本到图像的 AI 生成模型,而谷歌的 Muse 则实现了最先进的文本转换为图像的生成性能 [@rombachHighResolutionImageSynthesis2022]。另外,Flamingo 是一个视觉语言模型,能将图像、视频和文本作为提示输出相关语言 [@alayracFlamingoVisualLanguage];VisualGPT 是 OpenAI 推出的从图像到文本的生成模型 [@chenVisualGPTDataefficientAdaptation2022]。 126 | 127 | 人工智能内容产生过程中,难以避免遇到各种问题。例如偏见和歧视问题。由于训练数据集可能存在偏见和歧视,ChatGPT 可能会学习到这些偏见或歧视,因此需要采用多种方法对数据进行筛选和清洗,或者使用公平性算法来纠正模型偏差。总体而言,ChatGPT 的公平性取决于它的训练数据集以及使用它时的上下文和提问方式 [@brownLanguageModelsAre2020]。另外还有算力的挑战。ChatGPT 依赖着大量算力来训练海量文本数据,以此学习语言模式和知识。算力需求日益增长,致使这一领域存在着技术垄断,会对算力的进一步提升、大数据的训练等进一步行动产生影响。OpenAI 为了应对 ChatGPT 的高需求,已经采取了排队系统和流量整形等措施 [@morrisonComputePowerBecoming2022]。 128 | 129 | 根据本研究对相关成果的梳理,尚未发现详细分析与论述ChatGPT对科研工作者影响的研究论文。因此,本文准备从ChatGPT给科研工作者带来的机遇与挑战这两个方面作为切入点,展开论述。 130 | 131 | ## 机遇 132 | 133 | ChatGPT 是一种 AI 工程化的成功实践。AI 工程化专注于开发工具、系统和流程,使得人工智能能够应用于现实世界的领域[@WhatAIEngineer2022]。它使得普通人可以应用最新的自然语言生成与对话技术,完成很多曾经需要技术门槛才能完成的工作。 134 | 135 | ### 辅助编程 136 | 137 | 数据科学的研究范式已经深刻影响了许多学科。许多研究都需要通过不同形式来掌握足够的数据支持。通常,研究数据获取的途径主要有三种:开放数据集、API 调用和爬虫(Web Scrapper)。Python 语言是信息管理学科中进行数据分析最常用的编程语言。以前,用户必须掌握 Python 语言的基础语法,了解 Python 环境的使用,才能编写程序进行 API 调用或利用爬虫搜集数据。但现在有了 ChatGPT,用户可以通过自然语言对话的形式,给出要求,人工智能会直接给出源代码,并复制到实验环境中,从而获取所需数据。 138 | 139 | @fig-通过浏览器定位爬取范围 演示了研究者打算爬取某个网页上的信息,于是可以通过浏览器的 Inspector 功能查找对应的区域,此处找到的是类别 `sdyw_ul` 。 140 | 141 | ![通过浏览器定位爬取范围](assets/c92182ecda2fa2cc07d9efa8d766e22a50c4e73e.png){#fig-通过浏览器定位爬取范围} 142 | 143 | 有了对应的爬取位置,用户就可以在 ChatGPT 里面直接提出要求:“我想获得 sdyw_ul 类下的链接和标题。”(I want to get the links and titles under the class “sdyw_ul”)。然后,ChatGPT 自动编写程序,结果如 @fig-chatgpt自动编程爬虫 所示。 144 | 145 | ![ChatGPT 自动编程爬虫](assets/415dce89d5c8cf640fc2cda22e790af377419d47.png){#fig-chatgpt自动编程爬虫} 146 | 147 | 如果用户对程序运行结果不满意,可以通过进一步对话交流,让 ChatGPT 对程序源代码进行修改。例如可以通过这样的对话,让 ChatGPT 把数据输出的方式改成 CSV 文件。ChatGPT更新代码,返回结果如 @fig-chatgpt爬虫编程修改输出格式 所示。 148 | 149 | ![ChatGPT 爬虫编程修改输出格式](assets/59daadf49bea15f072504a5b2ac4e8cb7870123a.png){#fig-chatgpt爬虫编程修改输出格式} 150 | 151 | 因为 ChatGPT 对多轮对话的记忆力,所以每次只需要提出进一步的要求,即可不断让 ChatGPT 编写符合用户目标的程序,最终完成预期目标。 152 | 153 | 154 | 155 | ![ChatGPT 生成爬虫的最终运行结果](assets/38d8d26310dc4e87c963b21bf040a127acb49f1f.png){#fig-chatgpt生成爬虫的最终运行结果} 156 | 157 | 最终,用户可以仅通过自然语言交互和拷贝 ChatGPT 生成结果代码并运行的方式,把该网站上全部感兴趣的内容,存入到 Excel 文件中,如 @fig-chatgpt生成爬虫的最终运行结果 所示。 158 | 159 | ChatGPT 辅助编程,还不止体现在数据采集环节。ChatGPT 的基础模型是 “GPT-3.5”,底层基础模型是在大量代码上训练的结果,称为 code-davinci-002 [@OpenAIAPI]。因此,ChatGPT 见识过大量产生于 2021 年第四季度之前的代码段,并且对代码上下文补全有较强的能力。在此之前的数据分析甚至是机器学习模型训练工作,都可以通过自然语言对话的方式,交给 ChatGPT 来进行。 160 | 161 | 例如下面的例子里,用户尝试让 ChatGPT 采用 LDA 对一系列英文新闻文本做出主题挖掘。提出的自然语言指令和ChatGPT 的应答如 @fig-chatgpt编写lda主题挖掘代码 所示。 162 | 163 | ![ChatGPT 编写 LDA 主题挖掘代码](assets/7c79163f4badebaa5a8b44147ccfaa6de5e8fa97.png){#fig-chatgpt编写lda主题挖掘代码} 164 | 165 | 用户只需将ChatGPT给出的代码复制运行,对应生成的 LDA 主题聚类可视化结果如 @fig-chatgpt辅助编程生成lda主题聚类可视化结果 所示。 166 | 167 | ![ChatGPT 辅助编程生成 LDA 主题聚类可视化结果](assets/6013fb547f5cc75cd4e8848de93facef89e772f0.png){#fig-chatgpt辅助编程生成lda主题聚类可视化结果} 168 | 169 | 如图可见,原本需要一定的编程基础才能完成的机器学习乃至数据可视化任务,都可以通过和 ChatGPT 自然语言对话方式来达成。 170 | 171 | 而如果用户觉得结果有不满意的地方,可以随时跟 ChatGPT 交互,从而做出订正。例如原本的代码中,ChatGPT 默认为我们采用中文停用词,并且还使用 jieba 软件包进行分词。我们可以要求 ChatGPT 改用英语停用词进行处理。ChatGPT会立即根据新的要求变动,给出更新后的代码,如 @fig-要求chatgpt改用英文停用词表 所示。 172 | 173 | ![要求 ChatGPT 改用英文停用词表](assets/5379d1ab3082a281d5586bff44fd1afa8230959e.png){#fig-要求chatgpt改用英文停用词表} 174 | 175 | 在这个例子中,ChatGPT 改用了 nltk 软件包,使用内置的英文停用词表,可以做出更加符合要求的结果。 176 | 177 | 不仅如此,在大部分 ChatGPT 生成的代码中,不仅会有详细的注释。代码完成后,ChatGPT 还会给出 @fig-chatgpt给出的代码附加解释 这样相应的解释。 178 | 179 | ![ChatGPT 给出的代码附加解释](assets/d32689e146cbe33b97be3908468dca43bd232b5b.png){#fig-chatgpt给出的代码附加解释} 180 | 181 | 这对于我们了解代码实际的功用,并且在其上进行修改迭代甚至是查找错误,都非常有帮助。对于想学习编程的入门级研究人员,也会起到显著的帮助作用。 182 | 183 | ### 辅助阅读 184 | 185 | 做研究,免不了要阅读文献。在信息管理学科,期刊数量众多,而且外文期刊所占比例很大,每年都涌现出很多的新文章需要阅读。及时对文章重点进行把握,有利于在科研进度上保持领先。但是众多的文献阅读、消化、理解,尤其是外文文献的阅读,也对本领域研究者的构成了较大的挑战。 186 | 187 | 有了 ChatGPT,研究者可以将外文论文中的内容直接输入进来,然后利用提问的形式,要求 ChatGPT 自动提炼重点内容。 188 | 189 | 我们就以描述 ChatGPT 同类模型 InstrctGPT 的论文 “Training language models to follow instructions with human feedback” [@ouyangTrainingLanguageModels2022] 中的 3.5 节 “Models” 为例,输入其中主体部分到 ChatGPT ,给出的提示词是 “请用中文从下面资料中提炼出至少三个重点”。输入内容如 @fig-chatgpt自动提炼重点输入部分 所示。 190 | 191 | ![ChatGPT 自动提炼重点输入部分](assets/8cc438293eefe605a65a13583449118087ffef6c.png){#fig-chatgpt自动提炼重点输入部分} 192 | 193 | @fig-chatgpt自动提炼重点输出部分 是 ChatGPT 给出的答案。可见ChatGPT可以正确理解用户的要求,并且对内容进行了正确的自动总结。 194 | 195 | ![ChatGPT 自动提炼重点输出部分](assets/a74fb60664043b908b7485719ade4f1be2a2f30e.png){#fig-chatgpt自动提炼重点输出部分} 196 | 197 | 在模型学习训练集材料中,已经接触过不少专有名词,所以我们甚至可以不进行任何输入,直接让 ChatGPT 帮助解释一些专有名词。例如 @fig-chatgpt自动提炼重点输出部分 里答案中出现了 “深度强化学习”,我们可以让 ChatGPT 尝试讲解其含义。输入的提示词为 “什么是‘深度强化学习’,在上述文稿里面的作用是什么?”。 @fig-解释深度强化学习 是 ChatGPT 给出的回答: 198 | 199 | ![解释深度强化学习](assets/cde71f85f2097fb2114f0f3dc5dfc1c833335e2a.png){#fig-解释深度强化学习} 200 | 201 | 我们可以对 @fig-解释深度强化学习 中出现的新专有名词继续提问,例如 “赌徒困境” 是什么?ChatGPT的回答如 @fig-chatgpt解释赌徒困境 所示。 202 | 203 | ![ChatGPT 解释赌徒困境](assets/a2ecd34240dce59307e24975b737bfe5818d4adc.png){#fig-chatgpt解释赌徒困境} 204 | 205 | 如果对 ChatGPT 总结的内容不放心,用户还可以让 ChatGPT 找到与专有名词对应的原文文本。 @fig-查找专有名词对应的原始文本 为ChatGPT自动找出的“赌徒困境”原始文本。 206 | 207 | ![查找专有名词对应的原始文本](assets/914e12bc7af6f7a3f5d8fc8e58133528201810af.png){#fig-查找专有名词对应的原始文本} 208 | 209 | 通过 ChatGPT 展示原文的文本,研究者可以加以印证,证明 ChatGPT 总结没有偏离原文的叙述。 210 | 211 | 另外,用户还可以对文本提出问题,ChatGPT 会尽全力尝试解答。例如示例论文这样的讲述模型训练方法的作品,研究者可能更感兴趣一种模型获取反馈与提升改进的流程,并且用它和其他同类模型进行比对。所以我们可以问出一个非常综合性的问题 “模型是如何获得反馈和改进,达到训练目标的?” @fig-chatgpt对阅读材料的综合性问题解答 是 ChatGPT 的回答。 212 | 213 | ![ChatGPT 对阅读材料的综合性问题解答](assets/02692ffe0470f7e6a8eca0694ea4c5d9e20ace0d.png){#fig-chatgpt对阅读材料的综合性问题解答} 214 | 215 | 可以看到,ChatGPT 对文本语义理解非常准确,而且还用中文进行了流畅自然的翻译。特别的,对于文中出现的专有名词,例如 “SFT” 等,都用英文全程和缩写加以注明。 216 | 217 | 因为 ChatGPT 具有多轮对话的记忆功能,用户甚至可以将多篇论文的主体部分分别输入,然后加以比对。这样一来,读论文的工作就从 “读一篇” 变成了 “读一片”,ChatGPT 的汇总可以快速提示研究者某一领域多篇重要文献的特点与异同,有助于研究者快速了解领域的发展与演化过程。在从前一个研究团队用若干天才能做出的文献梳理工作,ChatGPT 可以在很短时间内高效率完成。 218 | 219 | ### 辅助写作 220 | 221 | 写作是沟通科研工作成果的过程,必不可少。但是以往在写作环节,科研工作者往往需要花费很大的心力。因为不仅要详细描述和展示科研成果,也许要对行文的风格、措辞、举例等充分思虑和考量。特别是多人合作写文章的时候,往往还要第一作者最终统合稿件的不同部分,重新用统一的风格撰写全文。 222 | 223 | ChatGPT 本身的基础就是大语言模型,最善于学习语言风格。我们可以在草稿里面摆出主要的事实而不需要考虑语序、语法等因素,由 ChatGPT 来帮助我们写作、润色。 224 | 225 | 用户可以将自己之前写作的文章输入到 ChatGPT 中,然后如 @fig-chatgpt学习文本风格并存储 要求 ChatGPT 提取文章的风格。 226 | 227 | 228 | ![ChatGPT 学习文本风格并存储](assets/804c2261655137d213a686e090a5b14b58f4db42.png){#fig-chatgpt学习文本风格并存储} 229 | 230 | 之后,对于新的文本,可以调用存储的文章风格(本例中为 “paper style”)进行风格转化与改写。 231 | 232 | 例如 @fig-chatgpt以存储的风格改写文本示例 中演示的这个例子,是本文第一作者对第二作者提供材料的风格改写。 233 | 234 | ![ChatGPT 以存储的风格改写文本示例](assets/4f1a412c6bd71532488c09130f4e80b4b3b5b960.png){#fig-chatgpt以存储的风格改写文本示例} 235 | 236 | ChatGPT 对文本的语义加以保留,但是在表现形式上进行了调整变化。统一的风格样式可以提升读者阅读的流畅性。 237 | 238 | 在写作过程中,ChatGPT 也可以帮助作者扩展思路,联想到合适的例子。例如当写作过程中,发现当前使用的例子作为证据并不足够充分和贴切,需要找到更好的例证。在过去,如果用户需要找到相关信息,那就必须进入搜索引擎,输入关键词,然后在海量的结果中筛选适合的内容。然而现在,用户只需告诉 ChatGPT “补充例子,论证上面的论断”,就可以得到相关的信息,如 @fig-chatgpt补充例子证明论断 所示。 239 | 240 | ![ChatGPT 补充例子证明论断](assets/1e2153db432f74d65bab4a189cef40283fb5cfc1.png){#fig-chatgpt补充例子证明论断} 241 | 242 | 虽然 @fig-chatgpt补充例子证明论断 里 ChatGPT 提供的例子可能无法直接原文使用,但它至少对作者会有启发。例如人脸识别已经成为 “日用品”,用户几乎每天都要使用这种方式验证付款,但在写作时,作者或许没有第一时间想到将其作为 “AI 工程化” 的例子。 243 | 244 | 如果用户认为 @fig-chatgpt补充例子证明论断 中提供的例子不够好,可以接着要求 ChatGPT 提供其他的例子。ChatGPT返回的结果如 @fig-要求chatgpt继续补充例证 所示。 245 | 246 | ![要求 ChatGPT 继续补充例证](assets/c838c8c6e2a751a32701de778501987fa2ae9336.png){#fig-要求chatgpt继续补充例证} 247 | 248 | 这样一来,原本在写作中常见而琐碎的技术性问题,就被 ChatGPT 的人工智能功能解决,显著提升写作的效率。 249 | 250 | 小结一下,本节我们讨论了 ChatGPT 给研究者带来的机遇。通过辅助阅读、辅助写作与辅助编程,为研究者赋能,大幅提升研究工作的效率,缩短研究产生成果的时间周期。 251 | 252 | 但是,我们不应该只看到 ChatGPT 带来机遇的一面,它的出现也给不少研究者带来挑战和困扰。如果使用不当,甚至会给研究过程带来灾难性的后果。下一节我们来讨论 ChatGPT 带来的挑战。 253 | 254 | ## 挑战 255 | 256 | ChatGPT 给科研人员带来了便利与效率提升同时,也给科研工作带来了挑战。下面本节从回答的真实性、数据污染,以及隐私和数据安全角度分别论述。 257 | 258 | ### 回答的真实性 259 | 260 | ChatGPT 的基础是一个生成式语言模型,它根据概率分布关系生成最符合要求的语言,但无法保证生成内容的真实性和准确性。 261 | 262 | 一些研究者在使用 ChatGPT 时没有意识到这一点。他们惊讶于 ChatGPT 回答问题的精准性,并直接采纳其答案。对于前文列举的编程功能,这个问题并不严重,因为程序编码是否准确有客观的评价标准。但对于阅读和写作辅助功能,则可能会因缺乏足够的检验依据而导致研究者采纳错误的答案。 263 | 264 | 就以前文展示过的 ChatGPT 举例功能来说。作者曾经要求 ChatGPT 对 “人工智能工程化” 举出例证。结果收到的是 @fig-chatgpt的错误回答 这样的回答。 265 | 266 | 267 | ![ChatGPT 的错误回答](assets/e2d0f93e15ff597cdbc9d73b7936db7e17a52569.png){#fig-chatgpt的错误回答} 268 | 269 | 这个回答中的疏漏非常明显。DALLE 究竟是由 Facebook 还是 OpenAI 推出的?ChatGPT 给出的这两个例子间已经是自相矛盾了。用户不难发现,即便对答案的真实性缺乏把握,ChatGPT 回答时语气却非常自信。如果研究者在使用 ChatGPT 生成答案时不进行取舍,将其作为内容的组成部分,发表论文或者出版书籍后,难免遇到尴尬的情况。因此在选用 ChatGPT 的答案时,研究者应该保持审慎的态度。 270 | 271 | ### 数据污染 272 | 273 | ChatGPT 的广泛使用,使得很多未经思考或者验证的内容大量产生。根据 [Intelligent.com](http://Intelligent.com) 的报道,被调查的大学生中,至少有三分之一采用 ChatGPT 来撰写作业的答案 [@NearlyCollegeStudents]。ChatGPT 更是被广泛应用于问答网站的答案生产,并且大量充斥于社交媒体。虚假信息直接影响受众之外,这些海量产生的低质量信息,也会造成互联网数据的污染。 274 | 275 | 这就意味着,未来的人工智能模型,在从互联网获取大规模公开语料时,会吸纳大量的 ChatGPT 生成内容。如果不能加以有效甄别,这些数据将深刻影响未来模型训练数据的质量。人工智能需要从人类产生的文本学习语言的规律。如此多的人工生成数据涌入训练集,不仅不会对模型能力带来提升,还可能混入更多噪声,导致回答问题的准确度降低。这会反过来影响人类的信息获取与知识传承。 276 | 277 | OpenAI 指出 ChatGPT 的不正当使用,会对教育者、家长、记者、虚假信息研究人员和其他群体产生影响。为此,OpenAI 官方在 2023 年 1 月推出了 ChatGPT 生成文本的检测分类器 [@NewAIClassifier2023]。使用的演示效果(采用官网自带的人工输入文本)如 @fig-openai官方推出的chatgpt检测分类器 所示。 278 | 279 | ![OpenAI 官方推出的 ChatGPT 检测分类器](assets/9fd13f5a99e9a6362ab8cbb0cfa07f7d9180391b.png){#fig-openai官方推出的chatgpt检测分类器} 280 | 281 | 然而,目前这种分类器还存在着非常多的问题。OpenAI 官方建议不要将其作为主要决策工具,而应作为确定文本来源的辅助手段。对于短文本(少于 1000 个字符),OpenAI 提供的 ChatGPT 分类器不可靠。即使是较长的文本,有时也会被分类器错误标记。甚至人工编写的文本也时常会被分类器错误地标记为由 AI 编写,而且检测器对于错误的检测结果非常自信。 282 | 283 | OpenAI 建议仅将分类器用于英文文本。在其他语言上分类器表现要差得多。对于代码,该分类器也不可靠。另外,它无法可靠地识别有规律的可预测的文本。官方给出的例子是 “包含前 1000 个质数列表是由 AI 还是人类编写的,因为正确答案总是相同的”。 284 | 285 | AI 编写的文本通过编辑可以规避分类器识别。OpenAI 提供的分类器能够应对一定程度的改写,但对于改写较多的情况则不能准确识别。 286 | 287 | ChatGPT 带来的另外一个挑战,就是隐私与数据安全问题。 288 | 289 | 当用户第一次注册并开启 ChatGPT 时,会看到有一些有关数据收集和隐私保护的提示,如 @fig-chatgpt提示用户用户不要输入隐私信息 所示。 290 | 291 | ![ChatGPT 提示用户用户不要输入隐私信息](assets/41ddb56383c07584e4f50e4dfcaa142fa219f54a.jpg){#fig-chatgpt提示用户用户不要输入隐私信息} 292 | 293 | 许多人将 ChatGPT 视为成熟的产品来使用,并认为它保护用户隐私是理所当然的事情。然而,事实并非如此。ChatGPT 这个模型建立在 GPT3.5 版本之上,使用了人工参与的增强学习。每个 “研究预览版” 的 ChatGPT 用户都是 OpenAI 的免费测试员。 294 | 295 | 如果用户输入的内容包含敏感信息,如银行卡号,则可能会对用户的财务和金融安全造成影响。而如果输入手机号、住址等信息,则可能会带来人身安全的隐患。 296 | 297 | 对于研究者来说,在输入原创性想法时也要三思而行。尽管 ChatGPT 并没有主动剽窃用户想法的意图,但用户输入的内容都会对模型造成影响。如果恰巧有其他用户对同一研究方向感兴趣,前面研究者输入的想法可能会作为答案的一部分启发后者。另外根据 OpenAI 官方提示,ChatGPT 的人工训练员(AI trainers)也有可能查看对话信息,以改进模型。 298 | 299 | 从学术整体进步的角度来看,这种信息加速传播共享有利于研究进展速度。但对研究者个体来说,其利益可能会受到潜在的威胁。 300 | 301 | 小结一下,本节提到了 ChatGPT 带来的挑战,主要分为回答真实性、数据污染与隐私和安全几个方面。面对 ChatGPT 带来的挑战,研究者可以通过以下对策尽量避免潜在的损失。 302 | 303 | 首先,针对回答的真实性问题,建议研究者时刻警醒,不要轻易相信 ChatGPT 提供的答案。即便对看似合理的答案内容,在正式采纳和使用之前,也需要找到源头信息进行验证。 304 | 305 | 其次,针对数据污染问题,建议研究者采用 OpenAI 官方提供的 ChatGPT 生成文本检测工具对重要来源数据进行抽样检测。在构建大规模研究数据集时,尽量避免采用开放式问答社区 2022 年 12 月之后的回答,以避免噪声混入。 306 | 307 | 第三,对于隐私和安全问题,建议研究者与 ChatGPT 对话过程中,避免暴露个人隐私与所在机构的敏感信息。对于研究意图和想法,如果无法绕开,尽量分散在不同对话中进行任务处理,避免被人工训练员在同一对话中大量获取相关信息。 308 | 309 | ## 结论 310 | 311 | OpenAI 的对话机器人模型 ChatGPT 对科研工作者的外部环境造成了显著变化,为提高编程、阅读和写作效率带来了机遇,但也带来了回答真实性、数据污染和隐私安全等挑战。为了敏感地抓住 ChatGPT 的特点,创造竞争优势,科研工作者需要认真思考并采取行动。通过本文的讨论,读者可以看到 ChatGPT 对科研工作的赋能意义十分明显,合理利用的话能够大幅提升工作效率。针对 ChatGPT 给科研工作者带来的挑战,本文提出了一些对策。例如,在使用 ChatGPT 生成的答案时需要进行谨慎评估,同时需要利用的技术和方法来应对数据污染和隐私安全问题。总之科研工作者也需要不断学习和更新自己的技能,以更好地适应这个新的科研环境。 312 | 313 | ChatGPT 出现时间不久且快速迭代,同时也有许多的竞品宣布会在近期推出。但本文受到当前写作时间点的客观局限,无法对近期和远期即将出现产品或服务趋势做出准确预测。本文写作时,尚未发现与 ChatGPT 实力相当的真正竞品,因此研究对象比较单一,只涉及了 ChatGPT 自身。后续本团队会在新的同类产品出现后加以深入对比研究,为科研工作者提供更加符合本土化需求的分析结果与建议。 314 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from pathlib import Path 3 | import sys 4 | import pyperclip 5 | from dotenv import load_dotenv 6 | import os 7 | import yaml 8 | import json 9 | from typing import List, Dict, Any, Union, Callable 10 | import re 11 | import time 12 | from tqdm import tqdm 13 | import tiktoken 14 | from langchain_text_splitters import RecursiveCharacterTextSplitter 15 | from datetime import datetime, timedelta 16 | from contextlib import contextmanager 17 | import logging 18 | from exa_py import Exa 19 | from litellm import completion 20 | 21 | # Utility Functions 22 | @contextmanager 23 | def open_file_utf8(file_path: str, mode: str): 24 | """Safe file handling with UTF-8 encoding""" 25 | try: 26 | file = open(file_path, mode, encoding='utf-8') 27 | yield file 28 | finally: 29 | file.close() 30 | 31 | def setup_logging(log_dir: str = 'logs') -> logging.Logger: 32 | """Set up logging with file and console handlers""" 33 | current_dir = os.path.dirname(os.path.abspath(__file__)) 34 | log_dir = os.path.join(current_dir, log_dir) 35 | os.makedirs(log_dir, exist_ok=True) 36 | 37 | timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") 38 | log_file = os.path.join(log_dir, f'text_processor_{timestamp}.log') 39 | 40 | logging.basicConfig( 41 | level=logging.INFO, 42 | format='%(asctime)s - %(levelname)s - %(message)s', 43 | handlers=[ 44 | logging.FileHandler(log_file), 45 | logging.StreamHandler() 46 | ] 47 | ) 48 | return logging.getLogger(__name__) 49 | 50 | logger = setup_logging() 51 | 52 | # Configuration Classes 53 | class StrategyConfig: 54 | """Configuration for a single processing strategy""" 55 | def __init__(self, tool_name: str = None, model: str = None, 56 | prompt_name: str = None, input_format: str = None, 57 | output_name: str = None, tool_params: Dict[str, Any] = None): 58 | self.tool_name = tool_name 59 | self.model = model 60 | self.prompt_name = prompt_name 61 | self.input_format = input_format 62 | self.output_name = output_name 63 | self.tool_params = tool_params or {} 64 | 65 | class ConfigValidator: 66 | """Validates configuration structure and content""" 67 | @staticmethod 68 | def validate_config(config: Dict[str, Any]) -> None: 69 | required_keys = ['strategies'] 70 | for key in required_keys: 71 | if key not in config: 72 | raise ValueError(f"Missing required key in config: {key}") 73 | 74 | if not isinstance(config['strategies'], list): 75 | raise ValueError("'strategies' must be a list") 76 | 77 | for strategy in config['strategies']: 78 | if 'tool_name' not in strategy and 'model' not in strategy: 79 | raise ValueError("Each strategy must have either 'tool_name' or 'model'") 80 | 81 | if 'parameters' in config: 82 | if not isinstance(config['parameters'], dict): 83 | raise ValueError("'parameters' must be a dictionary") 84 | if 'tokens' in config['parameters'] and not isinstance(config['parameters']['tokens'], int): 85 | raise ValueError("'tokens' in parameters must be an integer") 86 | 87 | class ConfigManager: 88 | """Manages configuration loading and validation""" 89 | @staticmethod 90 | def load_config(workflow_name: str, custom_config_path: str = None) -> Dict[str, Any]: 91 | if custom_config_path: 92 | config_path = custom_config_path 93 | else: 94 | current_dir = os.path.dirname(os.path.abspath(__file__)) 95 | config_path = os.path.join(current_dir, 'config', f"{workflow_name}.yaml") 96 | 97 | if not os.path.exists(config_path): 98 | raise FileNotFoundError(f"Config file not found for workflow: {workflow_name}") 99 | 100 | try: 101 | with open_file_utf8(config_path, 'r') as f: 102 | config = yaml.safe_load(f) 103 | ConfigValidator.validate_config(config) 104 | return config 105 | except Exception as e: 106 | logger.error(f"Error loading configuration: {e}") 107 | raise 108 | 109 | # API Client 110 | class APIClient: 111 | """Client for API interactions""" 112 | def __init__(self, model: str): 113 | load_dotenv(override=True) 114 | self.model = model 115 | self.api_base = None 116 | self.api_key = None 117 | 118 | if model.startswith("openai/"): 119 | self.api_base = os.getenv('OPENAI_API_BASE') 120 | self.api_key = os.getenv('OPENAI_API_KEY') 121 | 122 | def query_api(self, messages: List[Dict[str, str]]) -> str: 123 | """Send query to API and handle response""" 124 | try: 125 | kwargs = {} 126 | if self.api_base: 127 | kwargs['api_base'] = self.api_base 128 | if self.api_key: 129 | kwargs['api_key'] = self.api_key 130 | 131 | response = completion( 132 | model=self.model, 133 | messages=messages, 134 | **kwargs 135 | ) 136 | return response.choices[0].message.content 137 | except Exception as e: 138 | logger.error(f"Error calling API for model {self.model}: {e}") 139 | raise 140 | 141 | # Text Processing 142 | class BaseTextProcessor: 143 | """Base class for text processing functionality""" 144 | def __init__(self, config: Dict[str, Any], default_max_tokens: int = 1000): 145 | self.max_tokens_per_chunk = config.get('parameters', {}).get('tokens', default_max_tokens) 146 | self.encoding = tiktoken.encoding_for_model("gpt-4-turbo") 147 | self.config = config 148 | self.chunk_count = 0 149 | self.current_chunk_number = 0 150 | self.memory = {} 151 | self.load_memory_files() 152 | 153 | def preprocess_text(self, text: str) -> str: 154 | """Preprocess text before splitting into chunks""" 155 | paragraphs = text.split('\n\n') 156 | processed_paragraphs = [] 157 | for paragraph in paragraphs: 158 | lines = paragraph.split('\n') 159 | processed_lines = [] 160 | for i, line in enumerate(lines): 161 | if i == 0 or not line.strip(): 162 | processed_lines.append(line) 163 | elif (len(line) > 0 and not line[0].isupper() and 164 | not line[0].isdigit() and i > 0 and 165 | len(lines[i-1].strip()) > 0 and 166 | lines[i-1].strip()[-1] not in '.!?.!?]'): 167 | processed_lines[-1] += ' ' + line.strip() 168 | else: 169 | processed_lines.append(line) 170 | processed_paragraphs.append('\n'.join(processed_lines)) 171 | return '\n\n'.join(processed_paragraphs) 172 | 173 | def split_text(self, text: str) -> List[str]: 174 | """Split text into processable chunks""" 175 | preprocessed_text = self.preprocess_text(text) 176 | chars_per_token = len(preprocessed_text) / len(self.encoding.encode(preprocessed_text)) 177 | max_chars = int(self.max_tokens_per_chunk * chars_per_token) 178 | 179 | logger.info(f"Preprocessed text length: {len(preprocessed_text)}") 180 | logger.info(f"Chars per token: {chars_per_token:.2f}") 181 | logger.info(f"Max chars per chunk: {max_chars}") 182 | 183 | separators = ["\n\n", "\n", ". ", "!", "?", ";", ",", ".", " ", ""] 184 | text_splitter = RecursiveCharacterTextSplitter( 185 | chunk_size=self.max_tokens_per_chunk, 186 | chunk_overlap=20, 187 | length_function=lambda t: len(self.encoding.encode(t)), 188 | separators=separators 189 | ) 190 | 191 | chunks = text_splitter.split_text(preprocessed_text) 192 | logger.info(f"Number of chunks after initial split: {len(chunks)}") 193 | 194 | return [self._split_chunk(chunk) if len(self.encoding.encode(chunk)) > 195 | self.max_tokens_per_chunk else chunk for chunk in chunks] 196 | 197 | def _split_chunk(self, chunk: str) -> str: 198 | """Split a chunk if it exceeds token limit""" 199 | sentences = re.split(r'(?<=[.!?])\s+', chunk) 200 | current_chunk = [] 201 | current_tokens = 0 202 | chunks = [] 203 | 204 | for sentence in sentences: 205 | sentence_tokens = len(self.encoding.encode(sentence)) 206 | if sentence_tokens > self.max_tokens_per_chunk: 207 | if current_chunk: 208 | chunks.append(" ".join(current_chunk)) 209 | chunks.append(sentence[:self.max_tokens_per_chunk]) 210 | current_chunk = [] 211 | current_tokens = 0 212 | elif current_tokens + sentence_tokens > self.max_tokens_per_chunk: 213 | chunks.append(" ".join(current_chunk)) 214 | current_chunk = [sentence] 215 | current_tokens = sentence_tokens 216 | else: 217 | current_chunk.append(sentence) 218 | current_tokens += sentence_tokens 219 | 220 | if current_chunk: 221 | chunks.append(" ".join(current_chunk)) 222 | return " ".join(chunks) 223 | 224 | def load_memory_files(self): 225 | """Load memory files from the memory directory""" 226 | current_dir = os.path.dirname(os.path.abspath(__file__)) 227 | memory_dir = os.path.join(current_dir, 'memory') 228 | if not os.path.exists(memory_dir): 229 | logger.warning(f"Memory directory not found: {memory_dir}") 230 | return 231 | 232 | for file_name in os.listdir(memory_dir): 233 | if file_name.endswith('.md'): 234 | file_path = os.path.join(memory_dir, file_name) 235 | key = file_name[:-3] 236 | try: 237 | with open_file_utf8(file_path, 'r') as f: 238 | self.memory[key] = f.read().strip() 239 | logger.info(f"Loaded memory file: {file_name}") 240 | except Exception as e: 241 | logger.error(f"Error reading memory file {file_name}: {e}") 242 | 243 | class TextProcessor(BaseTextProcessor): 244 | """Main text processing class""" 245 | def __init__(self, config: Dict[str, Any], default_max_tokens: int = 1000, 246 | verbose: bool = False, debug: bool = False): 247 | super().__init__(config, default_max_tokens) 248 | self.verbose = verbose 249 | self.debug = debug 250 | self.tools = self.load_tools() 251 | self.models = self.load_models() 252 | self.strategies = self.load_strategies() 253 | self.current_strategy = None 254 | self.run_timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") 255 | self.run_log_dir = self._create_run_log_dir() 256 | self.current_chunk_step = 0 # 添加当前chunk的step计数器 257 | 258 | def _create_run_log_dir(self) -> str: 259 | """创建运行日志目录""" 260 | log_dir = os.path.join( 261 | os.path.dirname(os.path.abspath(__file__)), 262 | 'logs', 263 | 'runs', 264 | self.run_timestamp 265 | ) 266 | os.makedirs(log_dir, exist_ok=True) 267 | return log_dir 268 | 269 | def log_strategy_step(self, strategy_name: str, step_number: int, 270 | input_data: Any, output_data: Any, error: str = None, 271 | model_name: str = None): 272 | """记录策略步骤的输入输出""" 273 | self.current_chunk_step += 1 # 增加当前chunk的step计数 274 | 275 | # 构建文件名前缀,使用current_chunk_step作为步骤编号 276 | file_prefix = f'chunk_{self.current_chunk_number:02d}_step_{self.current_chunk_step:02d}_{strategy_name}' 277 | if model_name: 278 | file_prefix = f'{file_prefix}_{model_name.replace("/", "_")}' 279 | 280 | # 记录输入 281 | input_path = os.path.join(self.run_log_dir, f'{file_prefix}_input.md') 282 | with open_file_utf8(input_path, 'w') as f: 283 | if isinstance(input_data, (dict, list)): 284 | f.write("```json\n") 285 | f.write(json.dumps(input_data, indent=2, ensure_ascii=False)) 286 | f.write("\n```") 287 | else: 288 | f.write(str(input_data)) 289 | 290 | # 记录输出 291 | output_path = os.path.join(self.run_log_dir, f'{file_prefix}_output.md') 292 | with open_file_utf8(output_path, 'w') as f: 293 | if isinstance(output_data, (dict, list)): 294 | f.write("```json\n") 295 | f.write(json.dumps(output_data, indent=2, ensure_ascii=False)) 296 | f.write("\n```") 297 | else: 298 | f.write(str(output_data)) 299 | 300 | # 如果有错误,记录错误 301 | if error: 302 | error_path = os.path.join(self.run_log_dir, f'{file_prefix}_error.md') 303 | with open_file_utf8(error_path, 'w') as f: 304 | f.write(f"# Error\n\n{error}") 305 | 306 | logger.info(f"Chunk {self.current_chunk_number}, Step {self.current_chunk_step} ({strategy_name}) logged to: {self.run_log_dir}") 307 | 308 | def load_tools(self) -> Dict[str, Callable]: 309 | """Load available tools""" 310 | tools = {} 311 | for strategy in self.config.get('strategies', []): 312 | if 'tool_name' in strategy: 313 | tool_name = strategy['tool_name'] 314 | if tool_name == 'exa_search': 315 | tools[tool_name] = SearchTools.exa_search 316 | return tools 317 | 318 | def load_models(self) -> Dict[str, APIClient]: 319 | """Load available models""" 320 | models = {} 321 | for strategy in self.config.get('strategies', []): 322 | if 'model' in strategy: 323 | model_name = strategy['model'] 324 | if model_name not in models: 325 | models[model_name] = APIClient(model_name) 326 | return models 327 | 328 | def load_strategies(self) -> List[Any]: 329 | """Load processing strategies""" 330 | return [ProcessingStrategy(strategy_config) 331 | for strategy_config in self.config.get('strategies', [])] 332 | 333 | def process_text(self, text: str) -> List[str]: 334 | """Process entire text through the pipeline""" 335 | chunks = self.split_text(text) 336 | self.chunk_count = len(chunks) 337 | logger.info(f"Text split into {self.chunk_count} chunks") 338 | return [self.process_chunk(chunk) for chunk in tqdm(chunks, desc="Processing chunks")] 339 | 340 | def process_chunk(self, chunk: str) -> str: 341 | """Process a single chunk through all strategies""" 342 | self.current_chunk_number += 1 343 | self.current_chunk_step = 0 # 重置当前chunk的step计数器 344 | if self.debug: 345 | logger.info(f"Processing chunk {self.current_chunk_number}/{self.chunk_count}") 346 | 347 | previous_outputs = {} 348 | final_result = "" 349 | 350 | for i, strategy in enumerate(self.strategies, 1): 351 | self.current_strategy = strategy 352 | try: 353 | result = strategy.process(chunk, self, previous_outputs) 354 | if result is not None: 355 | previous_outputs[strategy.config.output_name] = result 356 | final_result = result 357 | if self.debug: 358 | logger.info(f"Strategy {i} result: {result[:500]}...") 359 | except Exception as e: 360 | logger.error(f"Error in strategy {i}: {str(e)}") 361 | 362 | return final_result 363 | 364 | def execute_model(self, model_name: str, input_text: str) -> str: 365 | """Execute a model-based strategy""" 366 | if model_name not in self.models: 367 | raise ValueError(f"Model '{model_name}' not found") 368 | 369 | client = self.models[model_name] 370 | 371 | # 准备消息 372 | if isinstance(input_text, str): 373 | system_message = self.read_system_prompt(self.current_strategy.config.prompt_name) 374 | messages = [ 375 | {"role": "system", "content": system_message}, 376 | {"role": "user", "content": input_text} 377 | ] 378 | else: 379 | messages = input_text 380 | 381 | try: 382 | # 执行 API 调用 383 | response = client.query_api(messages) 384 | 385 | # 记录这一步的输入输出 386 | step_number = len(os.listdir(self.run_log_dir)) + 1 387 | strategy_name = self.current_strategy.config.prompt_name or "model_call" 388 | 389 | self.log_strategy_step( 390 | strategy_name=strategy_name, 391 | step_number=step_number, 392 | input_data=messages, 393 | output_data={"response": response}, 394 | model_name=model_name 395 | ) 396 | 397 | return response 398 | except Exception as e: 399 | error_msg = str(e) 400 | logger.error(f"Model execution error: {error_msg}") 401 | 402 | # 记录错误 403 | step_number = len(os.listdir(self.run_log_dir)) + 1 404 | strategy_name = self.current_strategy.config.prompt_name or "model_call" 405 | 406 | self.log_strategy_step( 407 | strategy_name=strategy_name, 408 | step_number=step_number, 409 | input_data=messages, 410 | output_data=None, 411 | error=error_msg, 412 | model_name=model_name 413 | ) 414 | 415 | raise 416 | 417 | def execute_tool(self, tool_name: str, chunk: str, tool_params: Dict[str, Any] = None) -> str: 418 | """Execute a tool-based strategy""" 419 | if tool_name not in self.tools: 420 | raise ValueError(f"Tool '{tool_name}' not found") 421 | 422 | merged_params = {**self.current_strategy.tool_params, **(tool_params or {})} 423 | 424 | try: 425 | # 执行工具调用 426 | result = self.tools[tool_name](chunk, **merged_params) 427 | 428 | # 记录这一步的输入输出 429 | step_number = len(os.listdir(self.run_log_dir)) + 1 430 | 431 | self.log_strategy_step( 432 | strategy_name=tool_name, 433 | step_number=step_number, 434 | input_data={ 435 | "chunk": chunk, 436 | "params": merged_params 437 | }, 438 | output_data={"result": result} 439 | ) 440 | 441 | return result 442 | except Exception as e: 443 | error_msg = str(e) 444 | logger.error(f"Tool execution error: {error_msg}") 445 | 446 | # 记录错误 447 | step_number = len(os.listdir(self.run_log_dir)) + 1 448 | 449 | self.log_strategy_step( 450 | strategy_name=tool_name, 451 | step_number=step_number, 452 | input_data={ 453 | "chunk": chunk, 454 | "params": merged_params 455 | }, 456 | output_data=None, 457 | error=error_msg 458 | ) 459 | 460 | raise 461 | 462 | def read_system_prompt(self, prompt_name: str) -> str: 463 | """Read system prompt from file""" 464 | current_dir = os.path.dirname(os.path.abspath(__file__)) 465 | prompt_path = os.path.join(current_dir, 'patterns', prompt_name, 'system.md') 466 | 467 | try: 468 | with open_file_utf8(prompt_path, 'r') as f: 469 | return f.read().strip() 470 | except Exception as e: 471 | logger.error(f"Error reading system prompt: {e}") 472 | raise 473 | 474 | # Strategy Implementation 475 | class ProcessingStrategy: 476 | """Implements a single processing strategy""" 477 | def __init__(self, config: Dict[str, Any]): 478 | self.config = StrategyConfig(**config) 479 | self.tool_params = self.config.tool_params or {} 480 | if self.config.input_format and self.config.prompt_name: 481 | self.user_input_template = self.config.input_format 482 | else: 483 | self.user_input_template = "{{text}}" 484 | 485 | def process(self, chunk: str, processor: TextProcessor, previous_outputs: Dict[str, str]) -> str: 486 | """Execute the strategy on a text chunk""" 487 | logger.debug(f"Processing strategy: {self.config.prompt_name or self.config.tool_name}") 488 | 489 | try: 490 | if self.config.tool_name: 491 | result = processor.execute_tool(self.config.tool_name, chunk, self.tool_params) 492 | elif self.config.model: 493 | prompt = self.prepare_prompt(chunk, processor, previous_outputs) 494 | result = processor.execute_model(self.config.model, prompt) 495 | else: 496 | raise ValueError("Neither tool_name nor model specified in strategy") 497 | 498 | return result if result is not None else "" 499 | except Exception as e: 500 | logger.error(f"Strategy execution error: {str(e)}") 501 | return "" 502 | 503 | def prepare_prompt(self, chunk: str, processor: TextProcessor, previous_outputs: Dict[str, str]) -> Union[str, List[Dict[str, str]]]: 504 | """Prepare prompt for model-based strategy""" 505 | # If chunk is already in messages format, return it directly 506 | if isinstance(chunk, list) and all(isinstance(m, dict) for m in chunk): 507 | return chunk 508 | 509 | # Format user input using template 510 | user_input = self.user_input_template 511 | 512 | # Replace previous outputs 513 | for key, value in previous_outputs.items(): 514 | user_input = user_input.replace(f"{{{{{key}}}}}", str(value)) 515 | 516 | # Replace current chunk 517 | user_input = user_input.replace("{{text}}", chunk) 518 | 519 | # Replace memory placeholders 520 | for key, value in processor.memory.items(): 521 | user_input = user_input.replace(f"{{{{memory_{key}}}}}", str(value)) 522 | 523 | return user_input 524 | 525 | # Search Tools Implementation 526 | class SearchTools: 527 | """Implementation of search-related tools""" 528 | @staticmethod 529 | def exa_search(query: str, **kwargs) -> str: 530 | """Execute search using Exa API""" 531 | logger.info(f"Executing Exa search with query: {query}") 532 | exa = Exa(os.getenv("EXA_API_KEY")) 533 | 534 | query_lines = query.strip().split('\n') 535 | actual_query = query_lines[0] 536 | 537 | search_params = { 538 | "query": actual_query, 539 | "num_results": 10, 540 | "start_published_date": None, 541 | "use_autoprompt": True, 542 | "category": "tweet", 543 | "text": {"max_characters": 2000}, 544 | "highlights": { 545 | "highlights_per_url": 2, 546 | "num_sentences": 1, 547 | "query": f"This is the highlight query: {actual_query}" 548 | } 549 | } 550 | 551 | # Update with provided parameters 552 | search_params.update(kwargs) 553 | 554 | # Handle date parameter 555 | if len(query_lines) > 1 and query_lines[1].isdigit() and len(query_lines[1]) == 8: 556 | date_str = query_lines[1] 557 | search_params['start_published_date'] = ( 558 | f"{date_str[:4]}-{date_str[4:6]}-{date_str[6:]}T00:00:00.000Z" 559 | ) 560 | elif search_params['start_published_date'] is None and search_params['category'] == "tweet": 561 | search_params['start_published_date'] = ( 562 | datetime.now() - timedelta(hours=72) 563 | ).strftime("%Y-%m-%dT%H:%M:%S.000Z") 564 | 565 | # Remove None values 566 | search_params = {k: v for k, v in search_params.items() if v is not None} 567 | 568 | logger.info(f"Search parameters: {json.dumps(search_params, indent=2)}") 569 | 570 | try: 571 | results = exa.search_and_contents(**search_params) 572 | logger.info(f"Exa {search_params['category']} search completed") 573 | return f'# Topic: {actual_query}\n{str(results)}' 574 | except Exception as e: 575 | logger.error(f"Exa search error: {str(e)}") 576 | return f"Error in Exa search: {str(e)}" 577 | 578 | # Output Management 579 | def save_output(results: List[str], output_path: str, output_format: str): 580 | """Save processing results to file""" 581 | output_handlers = { 582 | "json": lambda data, file: json.dump(data, file, indent=2, ensure_ascii=False), 583 | "md": lambda data, file: file.write("\n\n".join(filter(None, data))), 584 | "txt": lambda data, file: file.write("\n\n".join(filter(None, data))) 585 | } 586 | 587 | with open_file_utf8(output_path, "w") as f: 588 | output_handlers[output_format](results, f) 589 | 590 | # Main Application 591 | def main(): 592 | """Main application entry point""" 593 | load_dotenv(override=True) 594 | 595 | parser = argparse.ArgumentParser(description="Process text with configurable workflows.") 596 | parser.add_argument("input_file", type=str, help="Path to input text file") 597 | parser.add_argument("--workflow", type=str, required=True, help="Workflow type") 598 | parser.add_argument("--max_tokens", type=int, default=1200, help="Maximum tokens per chunk") 599 | parser.add_argument("--verbose", action="store_true", help="Enable verbose output") 600 | parser.add_argument("--config", type=str, help="Custom config file path") 601 | parser.add_argument("--output_format", type=str, default="md", 602 | choices=["md", "txt", "json"], help="Output format") 603 | parser.add_argument("--debug", action="store_true", help="Enable debug logging") 604 | 605 | args = parser.parse_args() 606 | 607 | try: 608 | # Load and validate configuration 609 | config = ConfigManager.load_config(args.workflow, args.config) 610 | if args.debug: 611 | logger.info("Config loaded successfully") 612 | logger.info(f"Config: {json.dumps(config, indent=2)}") 613 | 614 | # Initialize processor 615 | processor = TextProcessor(config, args.max_tokens, args.verbose, args.debug) 616 | 617 | # Setup input/output paths 618 | input_path = Path(args.input_file).resolve() 619 | output_path = input_path.parent / f"{args.workflow}-output.{args.output_format}" 620 | 621 | # Process text 622 | with open_file_utf8(input_path, "r") as f: 623 | text = f.read() 624 | 625 | if args.debug: 626 | logger.info("Input text loaded successfully") 627 | 628 | results = processor.process_text(text) 629 | 630 | if not results: 631 | logger.warning("No results generated") 632 | else: 633 | # Save results 634 | save_output(results, output_path, args.output_format) 635 | if args.debug: 636 | logger.info(f"Results saved to {output_path}") 637 | 638 | # Copy to clipboard 639 | with open_file_utf8(output_path, "r") as f: 640 | content = f.read() 641 | pyperclip.copy(content) 642 | if args.debug: 643 | logger.info("Content copied to clipboard") 644 | 645 | except FileNotFoundError as e: 646 | logger.error(f"File not found: {e}") 647 | sys.exit(1) 648 | except ValueError as e: 649 | logger.error(f"Invalid input: {e}") 650 | sys.exit(1) 651 | except Exception as e: 652 | logger.exception(f"Unexpected error: {e}") 653 | sys.exit(1) 654 | 655 | if __name__ == "__main__": 656 | main() --------------------------------------------------------------------------------