1. 项目概述在Elixir生态中优雅地集成本地大语言模型如果你和我一样既是Elixir的忠实拥趸又对当前AI浪潮中本地运行大语言模型LLM的能力着迷那么你很可能已经注意到了Ollama这个项目。它让在个人电脑或自有服务器上运行Llama、Mistral等开源模型变得像ollama run llama3.1一样简单。但当我们想把这些强大的模型能力无缝集成到自己的Elixir应用——无论是构建一个智能客服后端、一个代码生成工具还是一个需要复杂推理的自动化流程时直接去调用Ollama的HTTP API就显得有些笨拙了。我们需要处理连接、序列化、错误重试、流式响应等一系列繁琐的细节。这正是ollama-ex这个Elixir库诞生的意义它提供了一个类型安全、符合Elixir/Erlang并发哲学、且功能完整的Ollama API客户端让你能用纯正的Elixir方式与本地LLM对话。简单来说ollama-ex就是连接你的Elixir世界和本地大语言模型世界的那座桥。它不仅仅是一个简单的HTTP客户端包装更充分考虑了Elixir开发者的使用习惯。比如它原生支持将流式响应转换为Enumerable让你能用熟悉的Stream模块来处理token的逐块到达它也允许你将流直接导向任意进程如一个LiveView的进程完美契合Phoenix LiveView等实时应用的架构。此外对于当前LLM应用开发中的高级需求如工具调用Function Calling和结构化输出Structured Outputs它也提供了清晰、直观的接口。无论你是想快速验证一个想法还是构建一个生产级的AI增强应用这个库都能显著降低集成复杂度让你更专注于业务逻辑本身。2. 核心功能与设计哲学解析ollama-ex的设计目标很明确在Elixir中提供对Ollama API的完整、惯用且强大的访问能力。我们来看看它具体解决了哪些痛点以及其设计背后的考量。2.1 完整覆盖Ollama APIOllama自身提供了一套RESTful API涵盖了模型管理、生成、对话、嵌入等核心功能。ollama-ex的首要任务就是完整、准确地映射这些端点。这意味着你几乎可以用Elixir代码完成所有在Ollama命令行或HTTP接口中能做的事情从拉取模型ollama pull、列出模型到最核心的文本补全/api/generate和聊天/api/chat。库的API设计通常与Ollama官方的参数命名保持一致降低了学习成本。例如当你需要调整生成文本的“创造性”时你会直接使用:temperature参数而不是去记忆一个库自己发明的新名字。2.2 流式处理的原生集成流式响应是LLM交互中的关键特性它能极大地提升用户体验避免长时间等待。ollama-ex对此提供了两种Elixir风格的原生支持模式这是它的一大亮点。第一种模式是返回一个惰性的Enumerable。当你设置stream: true时库会返回一个{:ok, stream}元组。这个stream是一个标准的Elixir流你可以用Stream.map/2、Stream.filter/2等函数对其进行变换也可以用Enum.to_list/1将其物化或者通过Stream.each/2将每个token块发送到指定进程。这种方式非常函数式适合在纯数据处理管道中使用。第二种模式是直接将流导向一个进程PIDstream: self()。在这种模式下库会启动一个后台任务Task来处理HTTP请求并将收到的每一个数据块以消息的形式发送到你指定的进程。这种方式完美契合了OTPOpen Telecom Platform应用和Phoenix LiveView的架构。在LiveView中你的视图进程本身就是一个消息处理器可以直接接收这些流式token并实时更新页面无需引入额外的状态管理或轮询机制。这种设计体现了“让框架适应语言并发模型而非相反”的思想。2.3 高级功能工具调用与结构化输出随着模型能力的进化简单的文本问答已不能满足复杂应用场景。ollama-ex紧跟Ollama自身的更新提供了对工具调用和结构化输出的支持。工具调用Tool Use/Function Calling允许模型在推理过程中“决定”调用一个外部函数工具并将调用结果纳入后续的思考中。ollama-ex的接口清晰地定义了这个交互流程首先你在请求中通过:tools参数定义可用的工具列表遵循OpenAI的格式模型在回复中可能会在tool_calls字段里包含一个或多个工具调用请求你的应用需要拦截这些请求执行实际代码如查询数据库、调用外部API然后将执行结果以role: “tool”的消息格式再次发送给模型最终模型会结合工具返回的结果生成面向用户的最终回答。这个过程可能需要多次网络往返库的职责是让每次请求的构建变得简单。结构化输出Structured Outputs则解决了让模型返回规整数据的问题。通过:format参数你可以指定一个JSON Schema要求模型严格按照这个格式生成JSON字符串。这对于需要将LLM输出直接解析为Elixir结构体struct并用于后续逻辑处理的场景至关重要比如从一段自由文本中提取联系人信息、生成特定格式的API调用参数等。它减少了后处理的复杂性提高了数据可靠性。2.4 错误处理与配置一个健壮的库必须妥善处理网络异常、模型加载失败、参数错误等情况。ollama-ex的函数通常返回{:ok, response}或{:error, reason}这样的标准Elixir元组便于你用case语句或with宏进行模式匹配和错误传播。客户端初始化Ollama.init/1也支持自定义Ollama服务器的地址、端口、请求超时等配置方便你连接远程或容器内的Ollama实例。3. 从安装到“Hello World”快速上手实践理论说得再多不如动手一试。让我们从零开始创建一个新的Mix项目并集成ollama-ex完成第一次与本地LLM的对话。3.1 环境准备与依赖安装首先确保你的系统上已经安装了Ollama。如果还没有可以去Ollama官网下载安装包或者使用包管理器如macOS的brew安装。安装后在终端运行ollama run llama3.1来拉取并测试一个模型比如Llama 3.1 8B。这个步骤会下载约5GB的模型文件请确保网络通畅和磁盘空间充足。注意模型首次运行时会自动下载。你可以通过ollama list查看本地已有模型ollama pull model-name拉取新模型。选择模型时需考虑你的硬件尤其是GPU VRAM8B参数模型通常需要8GB以上显存才能流畅运行。接下来创建一个新的Elixir项目如果你还没有mix new my_ai_app cd my_ai_app打开项目根目录下的mix.exs文件在deps函数中添加ollama依赖。如项目README所示目前最新稳定版是~ 0.9。defp deps do [ {:ollama, ~ 0.9}, # ... 其他依赖 ] end保存文件后在项目目录下运行mix deps.get来获取依赖。Elixir的包管理器Hex会处理剩下的事情。3.2 初始化客户端与首次对话依赖安装完成后我们就可以在IExElixir的交互式Shell中开始实验了。在项目目录下运行iex -S mix来启动一个加载了当前项目的IEx会话。首先初始化一个Ollama客户端。默认情况下它会连接http://localhost:11434这也是Ollama服务的默认地址。client Ollama.init() # %Ollama.Client{base_url: http://localhost:11434, ...}现在让我们问一个经典问题。我们将使用llama3.1模型确保你已通过ollama pull llama3.1下载了它。{:ok, response} Ollama.completion(client, [ model: llama3.1, prompt: 用中文解释一下Elixir语言的Actor模型。, ]) IO.puts(response[response])执行这段代码你会看到模型生成的关于Elixir/Erlang Actor模型的解释。response是一个Map其中“response”字段包含了完整的生成文本。你还可以看到其他元信息如“created_at”、“model”等。3.3 进行多轮对话单次补全适合问答但更复杂的交互通常是多轮的。这时应该使用chat接口它维护一个消息历史列表。messages [ %{role: system, content: 你是一位精通Elixir和分布式系统的专家回答要简洁专业。}, %{role: user, content: 在Elixir中Task.async_stream 和 Task.Supervisor 分别适用于什么场景} ] {:ok, chat_response} Ollama.chat(client, model: llama3.1, messages: messages) assistant_reply chat_response[message][content] IO.puts(助手回复: #{assistant_reply}) # 将助手的回复加入历史继续对话 new_messages messages [%{role: assistant, content: assistant_reply}, %{role: user, content: 如果其中一个任务失败async_stream 会怎么处理}] {:ok, next_response} Ollama.chat(client, model: llama3.1, messages: new_messages) IO.puts(后续回复: #{next_response[message][content]})通过维护一个messages列表你可以轻松构建出复杂的多轮对话逻辑。role可以是“system”设定助手行为、“user”用户输入或“assistant”助手的历史回复。4. 深入核心流式响应、工具调用与结构化输出实战掌握了基础对话后我们来深入探索ollama-ex的几个高级特性这些是构建现代化AI应用的关键。4.1 流式响应处理详解流式响应能让你看到模型“思考”的过程对于生成长文本或构建实时交互界面至关重要。方案一使用Enumerable流这种方式将流式响应包装成一个惰性序列非常灵活。{:ok, token_stream} Ollama.completion(client, [ model: llama3.1, prompt: 写一首关于编程的短诗。, stream: true, ]) # 方式A收集所有token并拼接 full_poem token_stream | Stream.map(fn chunk - chunk[response] end) | Stream.reject(is_nil/1) | Enum.join() IO.puts(full_poem) # 方式B实时处理每个token例如打印到控制台 token_stream | Stream.each(fn chunk - token chunk[response] if token token ! do IO.write(token) # 模拟一些延迟让输出更易读 Process.sleep(20) end end) | Stream.run()方案二流式至指定进程推荐用于OTP应用这是与GenServer、LiveView等OTP行为集成的首选方式。defmodule MyApp.StreamingHandler do use GenServer def start_link(_) do GenServer.start_link(__MODULE__, nil) end def init(_) do # 初始化客户端 client Ollama.init() # 发起流式请求将结果发送给自己self() {:ok, task} Ollama.completion(client, [ model: llama3.1, prompt: 详细说明Phoenix LiveView的渲染机制。, stream: self(), # 关键流指向本进程 ]) # 将任务引用存入状态便于后续管理 {:ok, %{client: client, request_task: task, accumulated_text: }} end # 处理从流式任务发来的消息 def handle_info({task_pid, {:data, chunk}}, %{request_task: %Task{pid: task_pid}} state) do case chunk do %{done false, response token} when is_binary(token) - # 收到一个中间token IO.write(token) new_text state.accumulated_text token {:noreply, %{state | accumulated_text: new_text}} %{done true} - # 流结束 IO.puts(\n\n--- Stream Finished ---) {:noreply, state} _other - # 处理其他可能的消息格式 {:noreply, state} end end def handle_info({:DOWN, ref, :process, pid, reason}, state) do # 处理任务进程结束的消息正常结束或异常 IO.puts(Streaming task ended: #{inspect(reason)}) {:noreply, state} end end在这个GenServer示例中流式请求启动后每个token块都会以消息的形式发送到服务器的邮箱handle_info/2回调服务器可以实时处理如更新状态、广播到WebSocket。这是构建响应式AI应用的基石。4.2 实现工具调用Function Calling工具调用让模型具备了执行外部动作的能力。我们以一个“查询天气”和“计算器”工具为例构建一个简单的代理循环。首先定义我们的工具。工具定义是一个符合OpenAI函数调用格式的Map。defmodule MyApp.Tools do weather_tool %{ type: function, function: %{ name: get_current_weather, description: 获取指定城市的当前天气情况。, parameters: %{ type: object, properties: %{ location: %{ type: string, description: 城市名称例如北京San Francisco, CA。 }, unit: %{ type: string, enum: [celsius, fahrenheit], description: 温度单位默认为celsius摄氏度。 } }, required: [location] } } } calculator_tool %{ type: function, function: %{ name: evaluate_math_expression, description: 计算一个数学表达式的数值结果。支持加减乘除和括号。, parameters: %{ type: object, properties: %{ expression: %{ type: string, description: 数学表达式例如(3 4) * 5 / 2。 } }, required: [expression] } } } def tools, do: [weather_tool, calculator_tool] # 模拟的工具执行函数 def execute_tool(get_current_weather, %{location loc, unit unit}) do # 这里应该调用真实天气API我们模拟一个返回值 temperature Enum.random(15..35) condition Enum.random([晴天, 多云, 小雨, 阴天]) #{loc}的当前天气是#{condition}温度#{temperature}#{if unit \celsius\, do: \°C\, else: \°F\}。 end def execute_tool(evaluate_math_expression, %{expression expr}) do # 警告在生产环境中直接使用Code.eval_string是极其危险的容易导致代码注入。 # 这里仅为演示真实场景应使用安全的数学表达式解析库。 try do {result, _} Code.eval_string(expr) 表达式 #{expr} 的计算结果是 #{result}。 rescue _ - 无法计算表达式 #{expr}请检查格式是否正确。 end end def execute_tool(unknown_name, _args) do 错误未知的工具 #{unknown_name}。 end end接下来实现一个简单的代理循环处理用户输入、模型可能的工具调用、执行工具、并将结果反馈给模型直到模型给出最终回答。defmodule MyApp.ToolCallingAgent do alias MyApp.Tools def run(client, model, user_query) do # 初始消息历史 messages [%{role: user, content: user_query}] # 获取可用工具列表 available_tools Tools.tools() # 开始对话循环最多进行5轮工具调用以防无限循环 chat_loop(client, model, messages, available_tools, 5) end defp chat_loop(_client, _model, _messages, _tools, 0) do {:error, :too_many_tool_calls} end defp chat_loop(client, model, messages, tools, remaining_rounds) do # 1. 发送请求给模型附带工具定义 case Ollama.chat(client, model: model, messages: messages, tools: tools) do {:ok, %{message assistant_msg}} - # 检查回复中是否包含工具调用 case assistant_msg do %{tool_calls tool_calls} when is_list(tool_calls) and tool_calls ! [] - # 2. 模型要求调用工具 IO.puts([模型决定调用工具]) new_messages messages [assistant_msg] # 处理每一个工具调用 {tool_results, updated_messages} Enum.reduce(tool_calls, {[], new_messages}, fn tool_call, {results, msg_acc} - func tool_call[function] func_name func[name] func_args Jason.decode!(func[arguments]) IO.puts( 执行工具: #{func_name}(#{inspect(func_args)})) # 3. 执行实际工具 result_content Tools.execute_tool(func_name, func_args) IO.puts( 工具结果: #{result_content}) # 构建工具结果消息 tool_result_msg %{ role: tool, # tool_calls 中的每个调用都有一个唯一的 id需要传回 tool_call_id: tool_call[id], content: result_content } {[result_content | results], [tool_result_msg | msg_acc]} end) # 反转列表以保持顺序并将工具结果消息加入历史 tool_results_rev Enum.reverse(tool_results) updated_messages_rev Enum.reverse(updated_messages) # 4. 带着工具结果再次调用模型递归 chat_loop(client, model, updated_messages_rev, tools, remaining_rounds - 1) %{content final_answer} when is_binary(final_answer) and final_answer ! - # 模型给出了最终文本回答循环结束 IO.puts([模型给出最终回答]) {:ok, final_answer, messages [assistant_msg]} _other - # 模型回复了空内容或格式不符 {:error, :unexpected_response_format, assistant_msg} end {:error, reason} - {:error, reason} end end end现在让我们运行这个代理client Ollama.init() model qwen2.5:7b # 使用一个支持工具调用的模型如Qwen2.5 case MyApp.ToolCallingAgent.run(client, model, 今天北京天气怎么样然后帮我计算一下(1234)*2的值。) do {:ok, final_answer, _history} - IO.puts(\n 最终回答 \n#{final_answer}) {:error, reason} - IO.puts(出错: #{inspect(reason)}) end这个循环会先让模型决定调用get_current_weather工具我们模拟执行后返回天气结果模型收到天气结果后会继续处理用户请求的下一部分决定调用evaluate_math_expression工具最后模型结合两个工具的结果生成一段连贯的最终回答。整个过程完全自动化展示了智能体Agent的雏形。4.3 强制结构化输出对于需要精准数据提取的场景结构化输出是利器。假设我们要从一段产品描述中提取关键信息。product_description 最新款智能手机“光子X1”今日发布搭载了全新的“天玑9200”处理器配备了6.8英寸的AMOLED显示屏刷新率高达120Hz。 后置三摄系统主摄为5000万像素。内置电池容量为5000mAh支持100W有线快充和50W无线快充。 提供三种颜色星夜黑、冰川银、晨曦金。起售价为3999元人民币。 extraction_schema %{ type: object, properties: %{ product_name: %{type: string}, processor: %{type: string}, screen_size_inches: %{type: number}, refresh_rate_hz: %{type: integer}, battery_capacity_mah: %{type: integer}, fast_charging_w: %{type: integer}, colors: %{ type: array, items: %{type: string} }, starting_price_rmb: %{type: number} }, required: [product_name, processor, screen_size_inches, battery_capacity_mah, starting_price_rmb] } {:ok, response} Ollama.completion(client, [ model: llama3.1, prompt: 从以下文本中提取产品规格信息\n#{product_description}, format: extraction_schema, ]) case response do %{response json_str} - # 解析JSON字符串为Elixir Map {:ok, product_spec} Jason.decode(json_str) IO.inspect(product_spec, label: 提取的产品规格) # 现在你可以像使用普通Map一样使用这些数据了 IO.puts(产品: #{product_spec[product_name]}) IO.puts(处理器: #{product_spec[processor]}) IO.puts(价格: #{product_spec[starting_price_rmb]}元) _ - IO.puts(提取失败) end通过强制JSON Schema输出我们几乎可以直接将模型的回复反序列化为可用的数据结构极大地简化了后续的数据处理流程。5. 生产环境考量、常见问题与调优技巧将ollama-ex用于实际项目时有几个重要的实践点和坑需要注意。5.1 客户端配置与连接管理默认的Ollama.init/0适用于本地开发。在生产环境中Ollama服务可能部署在另一台服务器或容器内。# 自定义配置 client Ollama.init( base_url: System.get_env(OLLAMA_API_URL, http://localhost:11434), # 请求超时毫秒对于长文本生成可能需要增加 receive_timeout: 120_000, # 连接池配置如果使用HTTPoison等适配器 # adapter_opts: [pool: :ollama_pool] )建议将客户端配置放在应用的配置文件中如config/runtime.exs并使用环境变量来管理敏感或环境相关的设置。# config/runtime.exs import Config config :my_app, :ollama, base_url: System.get_env(OLLAMA_BASE_URL, http://localhost:11434), default_model: System.get_env(OLLAMA_DEFAULT_MODEL, llama3.1) # 在应用代码中 defmodule MyApp.Ollama do config Application.compile_env(:my_app, :ollama) def client do Ollama.init( base_url: config[:base_url], receive_timeout: 180_000 ) end def default_model, do: config[:default_model] end5.2 模型选择与性能调优Ollama支持众多模型选择适合任务的模型至关重要。模型名称 (示例)特点适用场景硬件建议 (最低)llama3.2:1b/llama3.2:3b参数少速度快内存占用低简单分类、提取、轻量对话资源受限环境CPU / 4GB RAMllama3.1:8b/qwen2.5:7b能力均衡性价比高通用聊天、代码生成、复杂推理、工具调用8GB GPU VRAM 或 16GB RAMllama3.2:70b/qwen2.5:72b能力最强知识广博高精度分析、复杂创作、研究48GB GPU VRAM (多卡)关键参数调优:temperature(默认0.8)控制随机性。越高接近1.0回答越多样、有创意越低接近0.0回答越确定、保守。对于代码生成或事实问答建议调低如0.2-0.5。:num_predict/:max_tokens限制生成的最大token数。根据任务合理设置避免无限生成。:top_p(默认0.9)核采样与temperature类似控制输出多样性。通常调整一个即可。:seed设置随机种子可以使相同输入的输出具有可重复性便于调试。# 一个用于代码生成的“严谨”配置 code_config [ model: codellama:7b, temperature: 0.2, top_p: 0.95, num_predict: 1024, # 可以添加上下文让模型更了解当前文件 context: defmodule MyApp.User do\n use Ecto.Schema\n import Ecto.Changeset\n\n schema \users\ do\n field :name, :string\n field :email, :string\n timestamps()\n end\n\n def changeset(user, attrs) do\n user\n | cast(attrs, [:name, :email])\n | validate_required([:name, :email])\n | unique_constraint(:email)\n end\nend, prompt: 为上面的User模型添加一个根据邮箱查找用户的函数函数名为 get_by_email。 ]5.3 错误处理与重试策略网络请求和模型推理都可能失败。必须实现健壮的错误处理。defmodule MyApp.RobustOllama do max_retries 3 base_delay_ms 500 def chat_with_retry(client, opts, retries_left \\ max_retries) do case Ollama.chat(client, opts) do {:ok, response} - {:ok, response} {:error, %{reason: :timeout}} when retries_left 0 - # 超时重试使用指数退避 delay base_delay_ms * :math.pow(2, max_retries - retries_left) | round() Process.sleep(delay) chat_with_retry(client, opts, retries_left - 1) {:error, %{status: 404}} - # 模型未找到可能是名称错误或未下载 {:error, :model_not_found} {:error, %{status: 500}} when retries_left 0 - # 服务器内部错误可能是Ollama服务暂时问题 Process.sleep(1000) chat_with_retry(client, opts, retries_left - 1) {:error, reason} - # 其他错误如网络错误、参数错误等直接返回 {:error, reason} end end end5.4 常见问题排查速查表在实际使用中你可能会遇到以下问题问题现象可能原因排查步骤与解决方案{:error, %{status: 404}}1. 模型名称拼写错误。2. 模型未下载到本地。1. 在终端运行ollama list确认模型名称。2. 运行ollama pull model-name下载模型。请求超时 (:timeout)1. 生成文本过长 (num_predict太大)。2. 模型太大硬件推理速度慢。3. Ollama服务无响应。1. 减少:num_predict。2. 换用更小的模型。3. 检查Ollama服务进程是否运行 (ollama serve)。4. 增加:receive_timeout配置。流式响应中断或不完整1. 网络连接不稳定。2. 消费流的进程崩溃。3. Ollama服务端中断。1. 检查网络。2. 在消费流的地方添加更完善的错误捕获 (try/rescue)。3. 考虑使用非流式接口或实现断点续传逻辑记录已收到的token。工具调用不生效1. 使用的模型不支持工具调用。2. 工具定义格式不正确。3. 提示词未引导模型使用工具。1. 使用明确支持工具调用的模型如mistral-nemo,qwen2.5:7b-instruct等。2. 严格对照OpenAI工具定义格式检查JSON Schema。3. 在系统提示词中明确说明“你可以使用以下工具”。内存使用量不断增长1. 长时间运行客户端或连接未正确释放。2. 在循环中不断创建新客户端。1. 复用客户端不要每次请求都Ollama.init/0。2. 如果使用流式至PID确保在任务完成后正确处理Task引用和监控。结构化输出格式不符1. 提供的JSON Schema太复杂或矛盾。2. 模型能力不足以理解复杂约束。1. 简化Schema优先使用基本类型和简单结构。2. 在提示词中更清晰地说明输出格式要求。3. 对输出进行后处理使用Jason.decode/1并处理可能的解析错误。5.5 与Phoenix LiveView集成的实战模式在LiveView中集成流式AI响应能创造极具吸引力的用户体验。核心模式是在handle_event中发起流式请求指向self()在handle_info中接收token并更新socket状态从而实时更新页面。defmodule MyAppWeb.ChatLive do use MyAppWeb, :live_view impl true def mount(_params, _session, socket) do # 初始化状态客户端、消息列表、当前是否在生成 client Ollama.init() {:ok, assign(socket, client: client, messages: [], is_generating: false, current_task_ref: nil)} end impl true def handle_event(send_message, %{text user_input}, socket) do if socket.assigns.is_generating do # 如果正在生成忽略新消息或提供排队机制 {:noreply, socket} else # 1. 将用户消息加入历史并更新UI user_msg %{role: :user, content: user_input} new_messages socket.assigns.messages [user_msg] socket assign(socket, messages: new_messages, is_generating: true) # 2. 准备发送给模型的完整消息历史转换为API格式 api_messages Enum.map(new_messages, fn %{role: r, content: c} - %{role: to_string(r), content: c} end) # 3. 发起流式请求结果发回本LiveView进程 {:ok, task} Ollama.chat(socket.assigns.client, [ model: llama3.1, messages: api_messages, stream: self(), ]) # 4. 在状态中保存任务引用并监控它 ref Process.monitor(task.pid) socket assign(socket, current_task_ref: ref, current_task_pid: task.pid, accumulated_reply: ) {:noreply, socket} end end impl true def handle_info({task_pid, {:data, %{done false, message %{content token}}}}, %{assigns: %{current_task_pid: task_pid}} socket) do # 收到一个流式token累加并更新UI new_accumulated socket.assigns.accumulated_reply token socket assign(socket, accumulated_reply: new_accumulated) # 可以在这里推送更新到前端例如每收到一个词就更新一次 {:noreply, push_event(socket, new_token, %{token: token})} end impl true def handle_info({task_pid, {:data, %{done true}}}, %{assigns: %{current_task_pid: task_pid}} socket) do # 流式生成结束将累积的完整回复加入消息历史 assistant_msg %{role: :assistant, content: socket.assigns.accumulated_reply} new_messages socket.assigns.messages [assistant_msg] socket socket | assign(messages: new_messages, is_generating: false, accumulated_reply: , current_task_pid: nil) | put_flash(:info, 回复生成完毕) {:noreply, socket} end impl true def handle_info({:DOWN, ref, :process, pid, reason}, %{assigns: %{current_task_ref: ref}} socket) do # 任务进程结束正常或异常 Process.demonitor(ref, [:flush]) socket assign(socket, is_generating: false, current_task_ref: nil, current_task_pid: nil) if reason ! :normal do # 非正常结束显示错误 {:noreply, put_flash(socket, :error, 生成过程出错: #{inspect(reason)})} else {:noreply, socket} end end # 忽略其他未知消息 def handle_info(_msg, socket), do: {:noreply, socket} end对应的LiveView模板需要能够处理前端通过push_event发送来的token并实时追加到聊天界面上。这种模式提供了类似ChatGPT的流畅打字机效果。6. 扩展思路与最佳实践在项目中使用ollama-ex一段时间后我总结出一些能提升开发体验和应用稳定性的实践。第一封装业务语义。不要在所有地方直接调用Ollama.chat/2。根据你的业务领域创建专属的上下文模块。defmodule MyApp.AI.CodeReviewer do system_prompt 你是一个资深的Elixir代码审查专家。你的任务是审查提供的代码片段指出潜在的性能问题、不符合惯例的写法、可能的安全漏洞并提供改进建议。请以清晰、友好的语气回复先总结主要问题再逐点详细说明。 def review_code(client, code_snippet) do messages [ %{role: system, content: system_prompt}, %{role: user, content: 请审查以下Elixir代码\nelixir\n#{code_snippet}\n} ] case Ollama.chat(client, model: codellama:7b, messages: messages, temperature: 0.3) do {:ok, %{message %{content feedback}}} - {:ok, feedback} {:error, reason} - {:error, reason} end end end第二实施速率限制和队列。如果你的应用可能面临高并发请求而Ollama后端资源有限需要防止请求过载。defmodule MyApp.AI.RateLimitedDispatcher do use GenServer # 使用 :hackney_pool 或类似库管理HTTP连接池 # 或者使用第三方库如 ex_rated 进行API调用频率限制 def start_link(_) do GenServer.start_link(__MODULE__, nil, name: __MODULE__) end def init(_) do # 初始化一个作业队列和限流器 {:ok, %{queue: :queue.new(), in_progress: nil, rate_limiter: ...}} end def submit_request(request_fun) do GenServer.call(__MODULE__, {:submit, request_fun}) end # ... 实现队列管理和限流逻辑顺序执行请求 end第三记录与监控。记录每次AI调用的模型、参数、输入token数、输出token数和耗时。这对于成本估算如果使用按token计费的云服务、性能分析和调试至关重要。defmodule MyApp.AI.InstrumentedClient do require Logger def chat(client, opts) do start_time System.monotonic_time() input_tokens estimate_tokens(opts[:messages]) # 需要实现一个估算函数 case Ollama.chat(client, opts) do {:ok, response} result - end_time System.monotonic_time() duration_ms System.convert_time_unit(end_time - start_time, :native, :millisecond) output_tokens estimate_tokens(response[message][content]) Logger.info([AI_CALL] model#{opts[:model]} in#{input_tokens} out#{output_tokens} dur#{duration_ms}ms) # 可以发送指标到StatsD/Prometheus :telemetry.execute([:ai, :chat, :completed], %{duration: duration_ms, input_tokens: input_tokens, output_tokens: output_tokens}, %{model: opts[:model]}) result {:error, reason} error - Logger.error([AI_CALL_FAILED] model#{opts[:model]} error#{inspect(reason)}) :telemetry.execute([:ai, :chat, :failed], %{}, %{model: opts[:model], error: reason}) error end end end最后保持依赖更新。ollama-ex和Ollama本身都在快速迭代。定期关注GitHub仓库的Release和Ollama的更新日志可以及时获得对新模型、新API特性如视觉模型、多模态的支持以及性能改进。