基于Streamlit与Gemini API构建轻量级AI代码生成工具实践
1. 项目概述一个基于Gemini的轻量级AI编码伴侣最近在尝试各种AI编码工具从GitHub Copilot到Cursor它们确实能极大提升效率但有时我只是想快速验证一个想法或者需要一个更轻量、更聚焦于“对话式”代码生成的工具。于是我动手搭建了CursorAISim——一个基于Streamlit和Google Gemini API的Web应用。它的核心目标很简单模拟主流AI编码助手的核心交互体验让你在一个简洁的网页里通过自然语言对话完成代码生成、解释、修正和管理的全流程。这个项目特别适合两类朋友一是想快速体验AI编程但不想安装复杂IDE插件的开发者二是希望将AI代码生成能力快速集成到自己项目或工作流中的技术爱好者。它本质上是一个“AI编码能力”的微服务前端你只需要一个浏览器和一个Gemini API密钥就能立刻获得一个功能完整的编码助手。下面我将从设计思路、核心实现到避坑经验完整拆解这个项目的构建过程。2. 核心架构与工具选型解析2.1 为什么选择Streamlit Gemini API的组合在构思这个项目时我评估了几个关键需求快速原型开发、极简的Web界面、强大的代码生成能力、以及易于维护的Python后端。Streamlit几乎是满足前两项的不二之选。它允许你用纯Python脚本构建交互式Web应用省去了前后端分离的复杂配置特别适合数据科学和AI工具类应用。你写的每一个st.text_input、st.button都会实时渲染成网页组件开发体验如同在写一个增强版的脚本。对于AI模型我选择了Google的Gemini API特别是gemini-2.0-flash模型。相较于其他方案它有几点优势首先在代码生成和理解任务上Gemini的表现非常稳定且符合预期生成的代码结构清晰注释也到位。其次其API设计简洁Python SDK (google-generativeai) 易于集成且免费额度对于个人项目和小规模使用完全足够。最后模型迭代速度快可以方便地切换到更新的版本如gemini-2.5-flash-preview以获得更强的能力。注意选择gemini-2.0-flash而非更复杂的gemini-pro是权衡了速度、成本与能力。对于代码生成这种“短平快”的任务flash版本响应速度极快通常在1-3秒内且效果足够好用户体验更流畅。2.2 数据层设计用Pydantic构建清晰的状态管理一个交互式应用的核心是状态管理。用户可能同时生成多个代码片段、进行多轮聊天对话这些状态需要在页面交互中持久化。Streamlit提供了st.session_state但直接使用字典容易导致结构混乱和类型错误。因此我引入了Pydantic来定义严格的数据模型。# 示例定义核心数据模型 from pydantic import BaseModel, Field from typing import List, Optional class CodeSnippet(BaseModel): 表示一个独立的代码片段 id: str # 唯一标识 filename: str # 建议的文件名 language: str # 编程语言 content: str # 代码内容 created_at: str # 创建时间戳 class ChatMessage(BaseModel): 表示一条聊天消息 role: str # user 或 assistant content: str # 消息内容 class AppState(BaseModel): 应用全局状态容器 chat_history: List[ChatMessage] Field(default_factorylist) code_snippets: List[CodeSnippet] Field(default_factorylist) current_action: str generate # 当前选中的功能这样设计的好处非常明显第一类型安全。Pydantic会在运行时验证数据比如确保language字段是字符串避免了因类型错误导致的诡异bug。第二结构自文档化。看一眼AppState类你就立刻明白整个应用需要管理哪些数据。第三与Streamlit无缝集成。我们可以将整个AppState实例存入st.session_state.app_state任何修改都会自动触发界面更新。2.3 前端布局双栏设计的交互逻辑为了模拟类似Cursor的“代码区”和“聊天区”并存的体验我采用了Streamlit的双栏布局st.columns。左栏专注于“代码的生成与管理”是一个生产区。这里放置了功能选择单选按钮、代码输入表单、以及所有已生成代码片段的列表。每个代码片段用st.expander折叠面板展示里面用st.code高亮显示并附带一个复选框用于下载选择。右栏则完全交给“对话与精修”是一个交流区。这里是一个完整的聊天界面使用st.chat_message和st.chat_input构建。所有与AI讨论代码、请求优化、修复错误的对话都在这里进行。一个关键的设计点是当AI在聊天回复中生成新的代码块时系统会尝试自动解析并将其作为新的CodeSnippet添加到左栏的管理列表中。这实现了从“讨论”到“产出”的无缝流转。3. 核心功能模块的深度实现3.1 Gemini API客户端的初始化与缓存与Gemini API的交互始于客户端的初始化。这里有一个重要的性能优化点使用st.cache_resource装饰器缓存客户端实例。因为每次用户交互如点击按钮都会从头到尾重新执行整个Streamlit脚本如果不缓存就会反复创建新的API客户端造成不必要的开销和延迟。import streamlit as st import google.generativeai as genai from typing import Optional st.cache_resource def get_gemini_client(api_key: str) - Optional[genai.GenerativeModel]: 初始化并缓存Gemini客户端。如果API密钥无效返回None。 if not api_key: st.sidebar.error(请输入Gemini API密钥。) return None try: genai.configure(api_keyapi_key) # 核心选择模型。这里使用gemini-2.0-flash平衡速度与能力。 model genai.GenerativeModel(gemini-2.0-flash) # 可以在这里配置生成参数如temperature创造性和max_output_tokens长度 # generation_config genai.GenerationConfig(temperature0.7, max_output_tokens2048) # model genai.GenerativeModel(gemini-2.0-flash, generation_configgeneration_config) return model except Exception as e: st.sidebar.error(fAPI密钥配置失败: {e}) return None这里有个实操细节API密钥通过侧边栏的st.text_input输入类型设置为password以隐藏明文。只有当密钥输入且有效时后续的AI功能才被激活。这种“按需初始化”的方式既安全又节省资源。3.2 核心交互函数与AI的“单次对话”所有AI功能——生成、解释、聊天——最终都收敛到一个核心函数send_gemini_message。它负责构造合适的提示词Prompt调用Gemini API并处理响应。def send_gemini_message(client: genai.GenerativeModel, prompt: str, context: str ) - str: 发送消息到Gemini模型并获取响应。 Args: client: 初始化的Gemini模型客户端。 prompt: 用户的主要指令。 context: 可选的上下文信息如待解释的代码。 Returns: 模型的文本响应。 full_prompt prompt if context: # 将上下文信息更清晰地整合到提示中提高AI理解度 full_prompt f请基于以下上下文信息回答问题或完成任务 【上下文开始】 {context} 【上下文结束】 我的请求是{prompt} try: response client.generate_content(full_prompt) # 检查响应是否被安全过滤器拦截 if response.prompt_feedback.block_reason: return f请求因{response.prompt_feedback.block_reason}被阻止。请调整您的输入。 return response.text except Exception as e: return f调用API时出错: {e}为什么需要context参数在“解释代码”功能中context就是用户粘贴的代码在“通过聊天修正代码”时context可以是之前生成的某个代码片段。通过精心构造full_prompt我们引导AI更准确地理解任务边界避免它“自由发挥”偏离主题。3.3 代码生成与管理的实现细节“生成代码”功能看似简单但要做好需要处理不少细节。用户输入自然语言描述、期望的文件名和语言。我们需要构造一个明确的指令给AI。# 在“生成代码”表单提交后的处理逻辑 if action generate and user_prompt: # 构造系统指令让AI生成高质量代码 system_instruction f请生成一段{language}代码。 要求如下 1. 完整实现以下功能{user_prompt} 2. 代码应简洁、高效并包含必要的注释。 3. 生成的代码块不要包含Markdown的符号。 with st.spinner(f正在用Gemini生成{language}代码...): ai_response send_gemini_message(gemini_client, system_instruction) # 后处理清理响应移除可能存在的markdown代码块标记 cleaned_code ai_response.strip().strip().strip() if cleaned_code.startswith(language): cleaned_code cleaned_code[len(language):].strip() # 创建CodeSnippet对象并存入状态 new_snippet CodeSnippet( idstr(uuid.uuid4())[:8], # 生成简短ID filenamefilename if filename else fsnippet_{int(time.time())}.{language}, languagelanguage, contentcleaned_code, created_atdatetime.now().strftime(%H:%M:%S) ) st.session_state.app_state.code_snippets.append(new_snippet)这里有两个关键点第一是提示词工程。明确的指令“包含注释”、“不要Markdown符号”能显著提升输出质量。第二是响应后处理。AI有时会在代码前后加上python或这样的标记我们需要将其剥离得到纯净的代码字符串。代码片段的管理列表使用st.expander展示每个片段可独立展开/折叠。通过遍历st.session_state.app_state.code_snippets并动态生成复选框实现了多选下载功能。下载时将选中的代码片段内容写入临时文件然后用zipfile库打包最后通过st.download_button提供给用户。3.4 聊天式代码修正的上下文关联这是项目中最有趣也最具挑战的部分。目标是在聊天过程中让AI能“记住”或“看到”我们正在讨论的代码。我实现了一个简单的上下文关联机制在发送聊天消息时将最新生成或选中的代码片段作为上下文附加给AI。# 在聊天输入处理逻辑中 if user_chat_input: # 将用户消息加入历史 st.session_state.app_state.chat_history.append(ChatMessage(roleuser, contentuser_chat_input)) # 准备上下文尝试获取最新的代码片段 context_code if st.session_state.app_state.code_snippets: latest_snippet st.session_state.app_state.code_snippets[-1] context_code f当前参考的代码片段语言{latest_snippet.language}\n{latest_snippet.content} # 构造聊天提示强调这是一次关于代码的对话 chat_prompt f你是一个专业的编程助手。用户正在与你讨论代码。 {f以下是当前可能相关的代码\n{context_code} if context_code else 目前没有特定的代码上下文。} 请以友好、专业的态度回应用户的请求{user_chat_input} 如果用户请求修改、优化或解释代码请直接给出修改后的完整代码块。 with st.spinner(AI正在思考...): ai_reply send_gemini_message(gemini_client, chat_prompt) # 将AI回复加入历史 st.session_state.app_state.chat_history.append(ChatMessage(roleassistant, contentai_reply)) # **关键步骤**尝试从AI回复中提取新的代码块 extracted_code extract_code_from_response(ai_reply) if extracted_code: # 如果提取到代码将其作为新片段加入管理列表 new_snippet_from_chat CodeSnippet(...) st.session_state.app_state.code_snippets.append(new_snippet_from_chat)extract_code_from_response函数是一个简单的解析器它使用正则表达式在AI的回复文本中寻找被包裹的代码块。一旦发现就将其剥离并创建为新的CodeSnippet。这样聊天中诞生的代码成果就能自动归档到左侧的管理区形成了工作闭环。4. 部署、安全与性能优化实践4.1 本地运行与Streamlit Cloud部署本地运行非常简单在项目根目录执行streamlit run code.py即可。但如果你想分享给他人使用部署到云端是更好的选择。我选择了Streamlit Community Cloud因为它对开源项目免费且与Streamlit生态无缝集成。部署步骤将代码推送到GitHub公开仓库。登录 share.streamlit.io 。点击“New app”选择你的仓库、分支和主文件路径code.py。在高级设置中需要添加一个名为GEMINI_API_KEY的Secrets将你的API密钥填入值中。这样在应用代码中可以通过st.secrets[GEMINI_API_KEY]安全读取避免了将密钥硬编码在代码中或让每个用户手动输入。重要安全提示永远不要将API密钥直接写在代码里或提交到版本控制系统。使用环境变量或Streamlit Secrets来管理。在code.py中应优先检查st.secrets如果没有再回退到用户输入。4.2 应用状态持久化与会话管理Streamlit应用默认是“无状态”的每次交互都重跑脚本。st.session_state解决了单次会话内的状态保持。但如果你希望跨浏览器会话或标签页持久化数据如下次打开还能看到历史代码就需要引入外部存储。一个轻量级的方案是使用st.cache_data配合本地文件或简单的键值存储如pickle或sqlite3。import pickle import streamlit as st def load_persistent_state(user_id: str): 从文件加载特定用户的持久化状态 try: with open(fstate_{user_id}.pkl, rb) as f: return pickle.load(f) except FileNotFoundError: return AppState() # 返回初始状态 def save_persistent_state(user_id: str, state: AppState): 保存特定用户的状态到文件 with open(fstate_{user_id}.pkl, wb) as f: pickle.dump(state, f) # 在应用初始化时可以基于某种用户标识如IP或登录ID加载状态 # 注意这是一个简化示例生产环境需要更安全的用户识别和文件管理。请注意在Streamlit Cloud等无服务器环境中文件系统是临时的不适合持久化。此时应考虑使用数据库如SQLite withsqlite3但需注意写入限制或云存储服务。4.3 错误处理与用户体验打磨一个健壮的应用必须妥善处理各种异常。在CursorAISim中我重点关注了几类错误API密钥错误在get_gemini_client函数中捕获genai.AuthenticationError等异常并在侧边栏清晰提示用户。API调用失败网络超时、模型过载、内容安全拦截等。在send_gemini_message中使用try-except包裹将友好的错误信息返回给用户界面而不是让整个应用崩溃。用户输入错误例如在解释代码时提交了空内容。通过表单验证和条件判断在前端给出即时提示。状态不一致例如在聊天中引用了一个已被删除的代码片段。通过检查st.session_state.app_state.code_snippets的长度和索引有效性来避免。此外通过广泛使用st.spinner在AI思考时显示加载动画使用st.success/st.warning给出操作反馈以及合理的布局与说明文字可以显著提升用户体验让应用感觉更专业、更可靠。5. 常见问题排查与进阶技巧在实际开发和使用的过程中我遇到了一些典型问题这里总结出来供你参考。5.1 Gemini API响应慢或无响应问题现象点击生成按钮后界面长时间卡住最终可能超时。排查步骤检查网络首先确认你的网络环境可以正常访问Google的API服务。检查API密钥与配额登录Google AI Studio确认API密钥有效且未超出免费配额或用量限制。降低请求复杂度过长的提示词或要求生成非常复杂的代码可能导致响应时间变长。尝试将任务拆解。切换模型gemini-2.0-flash通常很快。如果使用了gemini-pro或预览版模型可能会更慢。可以在代码中临时切换回flash版本测试。添加超时处理在send_gemini_message函数中可以考虑使用timeout参数为genai.generate_content设置超时例如30秒并在捕获异常后给出友好提示。5.2 生成的代码格式混乱或包含多余文本问题现象AI返回的代码被包裹在奇怪的Markdown格式中或者前面有一段解释文字。解决方案这主要靠提示词优化和响应后处理。强化提示词在系统指令中明确强调“只输出代码不要任何解释”、“不要使用Markdown代码块”。改进后处理函数编写更健壮的extract_code_from_response或代码清理函数。可以使用多级策略先尝试用正则匹配lang...如果失败再尝试匹配无语言标注的...最后如果还是没有可以假设整个响应就是代码并记录日志以便分析。import re def extract_code_from_response(response_text: str) - str: 从AI响应中提取代码块。 # 模式1匹配带语言标识的代码块 pattern_with_lang r(?:python|javascript|java|cpp|c|go|rust|html|css|sql)[\s\S]*?\n([\s\S]*?) # 模式2匹配无语言标识的代码块 pattern_generic r\n([\s\S]*?) match re.search(pattern_with_lang, response_text, re.IGNORECASE) if not match: match re.search(pattern_generic, response_text) if match: return match.group(1).strip() # 如果没有找到代码块可以返回空字符串或进行其他处理 # 有时AI会把代码放在没有反引号的“代码段”里这里可以添加更复杂的启发式规则 return 5.3 聊天上下文丢失或关联错误问题现象在聊天中讨论代码A但AI却基于代码B给出了回答。解决方案这涉及到上下文管理策略的改进。显式代码选择在UI上增加一个下拉菜单让用户从“已管理代码片段”列表中明确选择当前聊天要关联的片段。将选中的片段ID存入状态并在构造聊天提示时使用它。聊天历史摘要当对话轮数变多时可以将之前的聊天历史进行摘要再作为上下文发送以避免触及模型的最大上下文长度限制。Gemini模型有token限制过长的历史会被截断。更清晰的提示在附加代码上下文时使用更醒目的分隔符和说明例如“【当前讨论的代码-开始】...【当前讨论的代码-结束】”帮助AI更好地识别。5.4 应用在Streamlit Cloud上加载缓慢问题现象部署的应用首次打开或交互时反应慢。优化方向依赖优化检查requirements.txt确保只包含最必要的包且版本固定。避免引入庞大或不必要的依赖。缓存策略充分利用st.cache_data和st.cache_resource。例如可以将模型客户端、一些静态配置数据缓存起来。代码拆分如果应用逻辑非常复杂考虑将部分不常变化的计算或数据加载过程移到缓存函数中。Streamlit配置在.streamlit/config.toml中调整配置例如设置[server]下的maxMessageSize或启用[runner]的优化选项具体需参考Streamlit最新文档。5.5 安全性与API密钥管理核心原则前端应用永远不要硬编码或直接暴露API密钥。最佳实践本地运行通过环境变量.env文件配合python-dotenv库读取或运行时输入来获取密钥。Streamlit Cloud部署务必使用Secrets Management。在本地开发时可以在.streamlit/secrets.toml文件中模拟但此文件绝不能提交到Git。在部署时通过Web界面设置Secrets。密钥轮转定期在Google AI Studio中更新API密钥并在部署平台更新对应的Secret。用量监控在Google Cloud Console为你的API密钥设置用量预算和警报防止意外超额产生费用。这个项目从构思到实现再到不断打磨让我对如何将大模型API快速产品化有了更深的理解。它不是要替代Cursor或Copilot这样的专业工具而是提供了一个高度可定制、可集成的轻量级替代方案。你可以基于此代码轻松更换后端模型比如接入OpenAI API或本地部署的Ollama或者增加新的功能模块如代码风格检查、单元测试生成。希望这份详细的拆解能帮助你理解其运作机制并激发你构建属于自己的AI工具。