Comments 13
В сущности, статья про делегирование, а вовсе не про LLM. И даже не про ИТ
Много терминов, но нет фрагментов кода. Ссылка на гитхаб, это, конечно, хорошо, но статьи без кода сложнее воспринимать
но статьи без кода сложнее воспринимать
Вам можно позавидовать! Я, программистские статьи, плохо воспринимаю без картинок. По принципу: «Лучше один раз потрогать, чем сто раз увидеть!» :) .
Код в гите
Картинка в начале статьи, ссылкой на plantuml
Также количество строк кода по файлам как "экономика", стоимость разных частей проекта. Тут видно, что треть кода (движок формул и парсинг XML стилей Excel) была делегирована LLM, быстро реализована и не требовала далее сложного тестирования и рефакторинга.
Зато пришлось повозиться с теми частями, корректность которых нельзя доказать компилятором. Например, последовательность обработки. Ошибся и привет.
А код я могу выложить. Скажите, какой. Могу выложить список сигнатур функций движка формул, включая публичные апи и приватные функции
Код
// src/services/cell_change_service.rs - Полностью обновленная версия
// изменение ячеек, пересчет формул, дельта
use std::collections::HashMap;
use serde::Deserialize;
use uuid::Uuid;
use crate::services::calc_service::ChangedCell;
use crate::domain::dto::{CellUpsertItem, ReportCellDto,SheetWithData};
use crate::domain::formulas;
use crate::domain::utils::{a1_to_rc, rc_to_a1};
use crate::infrastructure::report_registry::ReportRegistry;
use crate::infrastructure::repository::Repo;
use crate::infrastructure::user_ctx::UserCtx;
use crate::services::errors::ServiceError;
use crate::services::adapters::{fetch_cells, fetch_report, load_sheet_from_report};
#[derive(Clone, Debug)]
pub struct CellChangeService {
repo: Repo,
excel: ReportRegistry,
}
#[derive(Deserialize, Clone, Debug)]
pub struct CellChangeInput {
pub report_id: Uuid,
pub sheet_name: String,
pub r: u32,
pub c: u32,
pub value: String,
pub region_id: Option<i16>,
}
type CellValues = HashMap<(u32, u32), String>;
type Coord = (u32, u32);
impl CellChangeService {
pub fn new(repo: Repo, excel: ReportRegistry) -> Self {
Self { repo, excel }
}
pub async fn change_cell(
&self,
input: CellChangeInput,
user: UserCtx,
) -> Result<Vec<ChangedCell>, ServiceError> {
user.require_auth()?;
let filled_sheet = self.load_sheet_data(&input).await?;
let before = self.compute_full_state(&filled_sheet);
self.save_change(&input).await?;
let after = self.compute_state_with_change(&filled_sheet, &input);
Ok(self.find_formula_changes(&before, &after, &input))
}
/// Возвращает структуру листа и пользовательские данные
async fn load_sheet_data(
&self,
input: &CellChangeInput
) -> Result<SheetWithData, ServiceError> {
let report = fetch_report(&self.repo, input.report_id).await?;
let sheet_structure = load_sheet_from_report(&self.excel, &report, &input.sheet_name)?;
let db_cells = fetch_cells(&self.repo, input.report_id, &input.sheet_name).await?;
Ok(SheetWithData {
sheet_structure,
db_cells,
})
}
/// Сохраним изменения
async fn save_change(&self, input: &CellChangeInput) -> Result<(), ServiceError> {
let (value_num, value_text) = normalize_value(&input.value);
let item = CellUpsertItem {
address: rc_to_a1(input.r, input.c),
value_num,
value_text,
region_id: input.region_id,
row_index: None,
};
self.repo.upsert_cells(input.report_id, &input.sheet_name, &[item]).await?;
Ok(())
}
// --- Методы вычислений теперь принимают `FilledSheet` ---
/// Пересчитаем весь лист
fn compute_full_state(&self, filled_sheet: &SheetWithData) -> CellValues {
let mut values = self.merge_template_and_db(
&filled_sheet.sheet_structure.values,
&filled_sheet.db_cells,
);
self.apply_formulas(&mut values, &filled_sheet.sheet_structure.formulas);
values
}
/// Пересчитаем лист после изменения ячейки
fn compute_state_with_change(
&self,
filled_sheet: &SheetWithData,
input: &CellChangeInput,
) -> CellValues {
let mut values = self.merge_template_and_db_except(
&filled_sheet.sheet_structure.values,
&filled_sheet.db_cells,
(input.r, input.c),
);
values.insert((input.r, input.c), input.value.clone());
self.apply_formulas(&mut values, &filled_sheet.sheet_structure.formulas);
values
}
// --- Вспомогательные методы ---
/// Зальем пользовательские данные из БД в шаблон
fn merge_template_and_db(&self, template: &CellValues, db_cells: &[ReportCellDto]) -> CellValues {
let mut values = template.clone();
for cell in db_cells {
if let Ok(coord) = a1_to_rc(&cell.address) {
values.insert(coord, cell_to_string(cell));
}
}
values
}
/// Зальем пользовательские данные из БД в шаблон кроме указанной ячейки
fn merge_template_and_db_except(
&self,
template: &CellValues,
db_cells: &[ReportCellDto],
except: Coord
) -> CellValues {
let mut values = template.clone();
for cell in db_cells {
if let Ok(coord) = a1_to_rc(&cell.address) {
if coord != except {
values.insert(coord, cell_to_string(cell));
}
}
}
values
}
fn apply_formulas(&self, values: &mut CellValues, formulas: &HashMap<Coord, String>) {
let computed = formulas::compute_formulas(values, formulas);
values.extend(computed);
}
/// найдем изменения вычисляемых ячеек
fn find_formula_changes(
&self,
before: &CellValues,
after: &CellValues,
input: &CellChangeInput,
) -> Vec<ChangedCell> {
let changed = (input.r, input.c);
let mut res = Vec::new();
for (coord, new_val) in after {
if *coord == changed { continue; }
let old = before.get(coord).cloned().unwrap_or_default();
if old != *new_val {
res.push(create_change_record(coord, new_val, &input.sheet_name));
}
}
res
}
}
// --- Чистые функции-помощники ---
fn cell_to_string(cell: &ReportCellDto) -> String {
cell.value_text.clone()
.or_else(|| cell.value_num.map(|n| n.to_string()))
.unwrap_or_default()
}
fn create_change_record(coord: &Coord, value: &str, sheet_name: &str) -> ChangedCell {
let (value_num, value_text) = normalize_value(value);
ChangedCell {
sheet_name: sheet_name.to_string(),
r: coord.0,
c: coord.1,
address: rc_to_a1(coord.0, coord.1),
value_num,
value_text,
is_formula: true,
display_value: value.to_string(),
}
}
pub fn normalize_value(s: &str) -> (Option<f64>, Option<String>) {
let trimmed = s.trim();
if trimmed.is_empty() { return (None, None); }
let normalized = trimmed.replace(' ', "").replace(',', ".");
match normalized.parse::<f64>() {
Ok(n) => (Some(n), None),
Err(_) => (None, Some(trimmed.to_string())),
}
}
Тут ошибся и не нашел ошибки пока не сконцентрировал логику, выкинув в другие слои то, что к алгоритму не относится. Поэтому, весь код старательно разделил по слоям: тонкие контроллеры, умные сервисы, домены, репозитории.
Код в гите
Картинка в начале статьи, ссылкой на plantuml
В данном случае, с концепцией вашей статьи:
Парадокс сложности: почему сложное, но формализованное стало дешевле простого, но контекстно зависимого
я, совершенно, согласен. Настолько, что мне не требуются ни код, ни картинки для ее обоснования. Я бы и сам мог, вместо вас, порассуждать на эту тему. Например:
Сложность ведь разная бывает. Есть сложность элементная, а есть структурная. Элементов, всяких разных может быть много, но структура их взаимоотношений, может быть, простой, или, по крайней мере, понятной и обозримой. Это ваш первый случай. Во втором случае, элементов меньше, но расположены они в беспорядке, хаотично.
Примерно, как жена говорит мужу: «У тебя беспорядок на компьютерном столе. Дай, я наведу там порядок!». На что муж отвечает: «Ничего не трогай! Это не «беспорядок», это «творческий беспорядок»!» И, добавляет: «Беспорядок – это у тебя. На мой вопрос: «Где у нас сахар?», ты ответила: «В банке из под чая, на которой написано «соль»!».
Интересней было бы поговорить о сложности программных проектов. Как сделать, чтобы легко было понять собственный код, написанный несколько месяцев назад? Как правильно структурировать сам проект и код в нём? И как, вообще, «правильно» реализовывать программные модели? Желательно, на конкретных примерах.
Как сделать, чтобы легко понять собственный код, написанный несколько месяцев назад?
Ну, ты полезешь с частным вопросом "где сахар?"
Должна быть привычная или хотя бы логичная система как найти сахар. Потом возникнет вопрос
как вынуть банку с сахаром, высыпать в маленькую сахарницу на столе. И не сломать при этом ничего.
как отметить, что сахара стало меньше и скоро надо взять его в магазине
Прикольней, когда ты попадаешь на остров где воздушный шар, башня и швабры. Вот там поведение кода более непредсказуемое. И бесполезно делать документацию, нужно делать предсказуемую логику.
Сложность ведь разная бывает. Есть сложность элементная, а есть структурная. Элементов, всяких разных может быть много, но структура их взаимоотношений, может быть, простой, или, по крайней мере, понятной и обозримой.
LLM ломает привычную экономику проекта. Я бы сам не полез крутить свой движок формул и кишки XML внутри Excel. А тут оказалось, что это дешевле чем настраивать готовые библиотеки под потокобезопасность и даже дешевле чем придумать к нему батч обработку накопившихся запросов пользователей
Впрочем, на месте LLM вполне может быть и джун, вообще любое делегирование задач
Должна быть привычная или хотя бы логичная система как найти сахар.
Ты говоришь о привычных, повторяющихся действиях. Но, там все просто. Ошибся раз, во второй уже будешь чуть умнее. Т.е., метод «научного втыка» – рулит.
бесполезно делать документацию, нужно делать предсказуемую логику
Ну, да! Как в анекдоте: «–Ёжики станьте птицами! – Ура! А как? – Отстаньте, глупые! Я не тактик, я стратег!»
Я бы сам не полез крутить свой движок формул и кишки XML внутри Excel.
У китайцев есть опенсорсный проект «MyCell», аналог Эксела, поддерживающий работу xml-файлов. Я даже пытался приспособить его для автоматизации процессов для табельной, занимающейся учетом рабочего времени, с использованием моей системы учета (программно – аппаратной). Жаль, фирму ликвидировали, а так могло что-то получиться.
Вы сравнивали решение задачи с нуля и использование готовой библиотеки с помощью llm и пришли к выводу что с нуля нейрона пишет лучше? - Это совершенно логично - обучающая выборка намного больше для ванильного кода.
Вы пришли к выводу что четко формализованные до разработки требования лучше, чем генерируемые в процессе разработки? - давно известная истинна - без четкого т.з. результат х.з.
или это просто сложно написанная статья об очевидных вещах?
Вопрос масштаба.
Мне вот была ни разу не очевидно, что 450+250 строк в мне не знакомой предметной области будет написать проще, чем выбирать как бы хитро работать с многопотоком с уже готовым но не потокобезопасным движком
На таких объемах я бы свою треуголку на то, что код написанный llm будет работать не поставил. Чем больше код, тем выше вероятность что llm пойдет в разнос, и по результатам одного экскремента из статьи - можно только утверждать только что "возможно, что сгенерировать код с 0 в полном объеме будет проще, чем использовать готовое решение".
Но как будто это опять же и так очевидно.
"Интересный взгляд на делегирование задач LLM — особенно в части, где формализация снижает стоимость разработки.
неделю статья с битой ссылкой на гит и хоть бы кто написал
ну, код никто не читает, ясно
парадокса нет, оно же и раньше так работало. Все дело в пороге входа.
Человек знающий питон всё пытается сделать на питоне, человек со знанием c++ делает на с++.
Человеку, не знающему ничего из этого, проще было взять какой-то комбайн настраиваемых мышкой, а теперь вот проще задавать вопросы нейросети.
Парадокс сложности: почему сложное, но формализованное стало дешевле простого, но контекстно зависимого